实现Android拖拽按钮接听电话效果:
- 向中心拖拽左右两侧的控件,实现对应的功能;
- 添加动画效果;

1、SlideSwitchView.kt
import android.content.Context
import android.graphics.*
import android.os.Handler
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import androidx.core.content.ContextCompat
class SlideSwitchView(
context: Context?,
attrs: AttributeSet?,
defStyleAttr: Int,
defStyleRes: Int
) : View(context, attrs, defStyleAttr, defStyleRes) {
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : this(
context,
attrs,
defStyleAttr,
0
)
constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context?) : this(context, null)
//画中心扩散圆相关
private val centerCirPaint = Paint()//画中间大圆的画笔
private val offSetTopAndBottom = 1f / 2//中心圆的边距布局顶部所占总高的比例
private val centerCirWidthList = ArrayList<Float>()//中心圆的扩散的宽度
private val centerCirAlphaList = ArrayList<Int>()//中心圆的扩散的透明度
private val centerSpeed = 0.3f//中心圆的扩散速度
private var centerSpeedAdd = 0f//中心圆增加的的扩散速度
private val mDiffuseWidth = 20//扩散圆宽度(扩散圆与原始圆的半径的差值)
private var offSetTopOrBottom: Float = 0f//中心圆的顶部到控件顶部的距离(底部到控件底部的距离)
private var centerCirRadius: Float = 0f//中心圆的半径
private var centerRatio = 1f//中心圆的倍率大小
private val zoomRatio = 0.3f//中心圆变大时半径最多增加几倍
private var centerCirColor = R.color.color_D5E7FF//中心圆和扩散圆的颜色
private var CENTER_CIR_COLOR_ORIGINAL = R.color.color_D5E7FF//中心圆和扩散圆的默认颜色
private var CENTER_CIR_COLOR_LEFT_START = R.color.color_589EFF
private var CENTER_CIR_COLOR_RIGHT_START = R.color.color_FCA56B
private var CENTER_CIR_COLOR_LEFT_END =
R.color.color_FF8432//左侧小球移动到中心圆圆心时的中心圆和扩散圆的颜色 也做左侧小圆的颜色及左侧点的颜色
private var CENTER_CIR_COLOR_RIGHT_END =
R.color.color_1677FF//右侧小球移动到中心圆圆心时的中心圆和扩散圆的颜色 也做右侧小圆的颜色及右侧点的颜色
private var AUTO_RESTORE = false//左右侧小圆是否自动恢复
//画左右小圆相关
private val smallCirPaint = Paint()//画左右两个小圆的画笔
private var leftCirX = 0f//左边圆的圆心的X坐标
private var leftCirY = 0f//左边圆的圆心的Y坐标
private var rightCirX = 0f//右边圆的圆心的X坐标
private var rightCirY = 0f //右边圆的圆心的Y坐标
private val smallCirPaddingBound = 10f//左边圆距左边控件的边距 右边圆距控件右边的边距
private val smallCirShadowRadius = 10f//小圆的阴影半径
private var smallCirRadius = 0f//小圆的半径
private val smallRestoreOffset = 20//小圆恢复到原位置的速度
var alphaLeft = 255//左边小圆及点的透明度
var alphaRight = 255//右边小圆及点的透明度
var actionIsLeft = false//执行操作的是哪一边
//画点相关
private val pointPaint = Paint()//画点的画笔
private val leftPointList = ArrayList<Int>()//左边的点的透明度
private val rightPointList = ArrayList<Int>()//右边的点的透明度
private val pointCount = 4//点的个数
private val pointRadius = context?.dp2px(2)?.toFloat()!!//点的半径
//绘制文字相关
var leftText = "备勤"
var rightText = "上班"
private val textPaint = Paint()
val textMarginTop = context?.dp2px(4)!!//文字和图片的间距
//绘制图片相关
var leftImg = BitmapFactory.decodeResource(resources, R.drawable.img_change_prepare)
var rightImg = BitmapFactory.decodeResource(resources, R.drawable.img_change_work)
val imgHeight = context?.dp2px(26)!!
val imgWidth = context?.dp2px(26)!!
private var changedOffset = 10//状态改变的偏移量
var leftChangedListener: View.OnClickListener? = null//左侧拖拽完成触发事件
var rightChangedListener: View.OnClickListener? = null//右侧拖拽完成触发事件
init {
context?.let {
//中心扩散圆相关初始化
centerCirPaint.isAntiAlias = true
centerCirWidthList.add(0f)
centerCirAlphaList.add(200)
//左右小圆的相关初始化
smallCirPaint.color = ContextCompat.getColor(it, R.color.color_1677FF)
smallCirPaint.isAntiAlias = true
smallCirPaint.setShadowLayer(
smallCirShadowRadius,
0f,
5f,
ContextCompat.getColor(context, R.color.color_803E8EFF)
)
//点的相关初始化
for (a in 0 until pointCount) {//透明度从255赋值到55
leftPointList.add((a + 1) * (255 / pointCount))
rightPointList.add((a + 1) * (255 / pointCount))
}
pointPaint.color = ContextCompat.getColor(it, R.color.color_1677FF)
pointPaint.isAntiAlias = true
//文字初始化相关
textPaint.color = Color.WHITE
smallCirPaint.isAntiAlias = true
textPaint.textSize = context.sp2px(14)
//初始化绘图相关
startPointAnimal()
}
}
private fun startPointAnimal() {
Handler().postDelayed({
val leftEndPoint = leftPointList.last()
leftPointList.add(0, leftEndPoint)
leftPointList.removeAt(leftPointList.count() - 1)
val rightEndPoint = rightPointList.last()
rightPointList.add(0, rightEndPoint)
rightPointList.removeAt(rightPointList.count() - 1)
startPointAnimal()
}, 200)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
if (offSetTopOrBottom == 0f) {
offSetTopOrBottom = height * offSetTopAndBottom / 2
}
if (centerCirRadius == 0f) {
centerCirRadius = (height / 2).toFloat() - offSetTopOrBottom
}
if (smallCirRadius == 0f) {
smallCirRadius = centerCirRadius//和中心圆半径一样
}
if (leftCirX == 0f) {
leftCirX = smallCirRadius
}
val leftAutoRestore =
leftCirX < width / 2f - changedOffset - smallCirShadowRadius || AUTO_RESTORE//针对小球移动但没达到触发条件需要自动恢复
if (IS_FREE && leftAutoRestore && leftCirX > smallCirRadius) {//小球自动恢复到原位置
leftCirX -= smallRestoreOffset
if (leftCirX < smallCirRadius) {
leftCirX = smallCirRadius
}
}
if (leftCirY == 0f) {
leftCirY = height / 2f
}
val maxDistance =
smallCirRadius * 2 + mDiffuseWidth//小圆和中心圆挨着的时候圆心的最大距离:此处中心圆半径按初始值算:(smallCirRadius+centerCirRadius)/2 因初始值smallCirRadius和centerCirRadius相等 所以直接等于smallCirRadius
val leftOffset = width / 2 - leftCirX - smallCirPaddingBound//左侧小圆圆心离中心圆的距离
var doLeft = false//执行了左侧的就不执行右侧的
if (leftOffset > maxDistance) {
centerRatio = 1f
centerSpeedAdd = 0f
} else {
doLeft = true
val tempOffset = (1 - leftOffset / maxDistance) * zoomRatio
centerRatio = 1 + tempOffset
centerSpeedAdd = tempOffset
}
centerCirColor =
if (leftCirX >= width / 2f - changedOffset - smallCirShadowRadius) {//小圆在中心圆的圆心 变色
CENTER_CIR_COLOR_LEFT_END
} else {
CENTER_CIR_COLOR_ORIGINAL
}
if (rightCirX == 0f) {
rightCirX = width - smallCirRadius
}
val rightAutoRestore =
rightCirX > width / 2f + changedOffset + smallCirShadowRadius || AUTO_RESTORE//针对小球移动但没达到触发条件需要自动恢复
if (IS_FREE && rightAutoRestore && rightCirX < width - smallCirRadius) {//小球自动恢复到原位置
rightCirX += smallRestoreOffset
if (rightCirX > width - smallCirRadius) {
rightCirX = width - smallCirRadius
}
}
val rightOffset = rightCirX - width / 2//右侧小圆圆心离中心圆的距离
if (doLeft.not()) {
if (rightOffset > maxDistance) {
centerRatio = 1f
centerSpeedAdd = 0f
} else {
val tempOffset = (1 - rightOffset / maxDistance) * zoomRatio
centerRatio = 1 + tempOffset
centerSpeedAdd = tempOffset
}
centerCirColor =
if (rightCirX <= width / 2f + changedOffset + smallCirShadowRadius) {//小圆在中心圆的圆心 变色
CENTER_CIR_COLOR_RIGHT_END
} else {
CENTER_CIR_COLOR_ORIGINAL
}
}
if (rightCirY == 0f) {
rightCirY = height / 2f
}
val realCenterCirRadius = centerCirRadius * centerRatio
//计算圆点的间隔 (控件宽的一半-小圆距离控件的边距-中心圆的半径-小圆的阴影半径*2-点的直径*个数)/计算出的多少个空隔
val pointSpace =
(width / 2f - (smallCirRadius * 2) - smallCirPaddingBound - realCenterCirRadius - (smallCirShadowRadius * 2) - (pointRadius * 2 * pointCount)) / (pointCount - 1)
//绘制扩散圆
centerCirPaint.color = ContextCompat.getColor(context, centerCirColor)
for (i in 0 until centerCirAlphaList.count()) {
val alpha = centerCirAlphaList[i]
val radius = centerCirWidthList[i]
centerCirPaint.alpha = alpha
val cirWidth = realCenterCirRadius + radius
canvas?.drawCircle(width.toFloat() / 2, height.toFloat() / 2, cirWidth, centerCirPaint)
if (alpha > 0 && cirWidth * 2 < height) {
centerCirAlphaList[i] =
(alpha - (200f / (offSetTopOrBottom / (centerSpeed + centerSpeedAdd)))).toInt()
centerCirWidthList[i] = radius + centerSpeed + centerSpeedAdd
}
}
// 判断当扩散圆扩散到指定宽度时添加新扩散圆
if (centerCirWidthList[centerCirWidthList.count() - 1] >= mDiffuseWidth) {
centerCirAlphaList.add(200)
centerCirWidthList.add(0f)
}
// 超过最大数量扩散圆,删除最外层
if (centerCirWidthList.count() > offSetTopOrBottom / mDiffuseWidth) {
centerCirWidthList.removeAt(0)
centerCirAlphaList.removeAt(0)
}
// 绘制中心圆
centerCirPaint.alpha = 255
canvas?.drawCircle(
width.toFloat() / 2,
height.toFloat() / 2,
realCenterCirRadius,
centerCirPaint
)
//绘制点
pointPaint.color = ContextCompat.getColor(context, CENTER_CIR_COLOR_LEFT_END)
for (i in 0 until leftPointList.count()) {
val alpha = leftPointList[i]
pointPaint.alpha = alpha
val pointX =
(smallCirRadius + smallCirShadowRadius) * 2 + smallCirPaddingBound + pointRadius + (i * pointSpace)
if (pointX >= leftCirX + smallCirPaddingBound) {//不绘制被小球拖过的路径的点
canvas?.drawCircle(
pointX,
height / 2f,
pointRadius,
pointPaint
)
}
}
pointPaint.color = ContextCompat.getColor(context, CENTER_CIR_COLOR_RIGHT_END)
for (i in 0 until rightPointList.count()) {
val alpha = rightPointList[i]
pointPaint.alpha = alpha
val pointX =
width - smallCirPaddingBound - ((smallCirShadowRadius + smallCirRadius) * 2) - pointRadius - (i * pointSpace)
if (pointX < rightCirX - smallCirPaddingBound) {//不绘制被小球拖过的路径的点
canvas?.drawCircle(
pointX,
height / 2f,
pointRadius,
pointPaint
)
}
}
//绘制左右小圆
smallCirPaint.alpha = alphaLeft
if (alphaLeft < 255) {
alphaLeft += 5
if (alphaLeft > 255) {
alphaLeft = 255
}
}
smallCirPaint.setShadowLayer(
smallCirShadowRadius,
0f,
5f,
ContextCompat.getColor(context, CENTER_CIR_COLOR_LEFT_END)
)
val tempLeftX = leftCirX + smallCirPaddingBound
smallCirPaint.shader = LinearGradient(
tempLeftX,
leftCirY - smallCirRadius,
tempLeftX,
leftCirY + smallCirRadius,
ContextCompat.getColor(context, CENTER_CIR_COLOR_LEFT_START),
ContextCompat.getColor(context, CENTER_CIR_COLOR_LEFT_END),
Shader.TileMode.MIRROR
)
// smallCirPaint.color = ContextCompat.getColor(context, CENTER_CIR_COLOR_LEFT_END)
canvas?.drawCircle(
tempLeftX,
leftCirY,
smallCirRadius,
smallCirPaint
)
smallCirPaint.alpha = alphaRight
if (alphaRight < 255) {
alphaRight += 5
if (alphaRight > 255) {
alphaRight = 255
}
}
smallCirPaint.setShadowLayer(
smallCirShadowRadius,
0f,
5f,
ContextCompat.getColor(context, CENTER_CIR_COLOR_RIGHT_END)
)
smallCirPaint.shader = LinearGradient(
tempLeftX,
leftCirY - smallCirRadius,
tempLeftX,
leftCirY + smallCirRadius,
ContextCompat.getColor(context, CENTER_CIR_COLOR_RIGHT_START),
ContextCompat.getColor(context, CENTER_CIR_COLOR_RIGHT_END),
Shader.TileMode.MIRROR
)
// smallCirPaint.color = ContextCompat.getColor(context, CENTER_CIR_COLOR_RIGHT_END)
canvas?.drawCircle(
rightCirX - smallCirPaddingBound,
rightCirY,
smallCirRadius,
smallCirPaint
)
//绘制文字
val rect = Rect()
textPaint.getTextBounds(leftText, 0, leftText.length, rect)
canvas?.drawText(
leftText,
leftCirX + smallCirPaddingBound - rect.width() / 2,
leftCirY + rect.height() + textMarginTop,
textPaint
)
textPaint.getTextBounds(rightText, 0, rightText.length, rect)
canvas?.drawText(
rightText,
rightCirX - smallCirPaddingBound - rect.width() / 2,
rightCirY + rect.height() + textMarginTop,
textPaint
)
var imgLeft = (leftCirX + smallCirPaddingBound - imgWidth / 2).toInt()
var imgTop = (leftCirY - imgHeight).toInt()
var imgRight = imgLeft + imgWidth
var imgBottom = imgTop + imgHeight
// 指定图片在屏幕上显示的区域(原图大小)
val dst = Rect(imgLeft, imgTop, imgRight, imgBottom)
//绘制图片
canvas?.drawBitmap(leftImg, null, dst, null)//绘制左侧图片
imgLeft = (rightCirX - smallCirPaddingBound - imgWidth / 2).toInt()
imgTop = (rightCirY - imgHeight).toInt()
imgRight = imgLeft + imgWidth
imgBottom = imgTop + imgHeight
dst.set(imgLeft, imgTop, imgRight, imgBottom)
canvas?.drawBitmap(rightImg, null, dst, null)//绘制右侧图片
invalidate()
// canvas?.drawArc(centerRectF,90f,360f,true,centerCirPaint)
}
fun setLeftImg(img: Int) {
leftImg = BitmapFactory.decodeResource(resources, img)
}
fun setRightImg(img: Int) {
BitmapFactory.decodeResource(resources, img)
}
var leftCanMove = false//左侧图片是否可以移动
var rightCanMove = false//右侧图片是否可以移动
var leftOffsetX = 0f
var rightOffsetX = 0f
var IS_FREE = true//如果不是在触摸状态则自动释放小圆回到原位置
override fun onTouchEvent(event: MotionEvent?): Boolean {
event?.apply {
when (action) {
MotionEvent.ACTION_DOWN -> {
//点击在左侧圆范围内
if (leftCirX == smallCirRadius && rightCirX == width - smallCirRadius && x > smallCirPaddingBound && x < smallCirPaddingBound + smallCirRadius * 2 && y > height / 2 - smallCirRadius && y < height / 2 + smallCirRadius) {
leftCanMove = true
leftOffsetX = x
IS_FREE = false
}
//点击在右侧圆范围内
if (leftCirX == smallCirRadius && rightCirX == width - smallCirRadius && x > width - smallCirPaddingBound - smallCirRadius * 2 && x < width - smallCirPaddingBound && y > height / 2 - smallCirRadius && y < height / 2 + smallCirRadius) {
rightCanMove = true
rightOffsetX = x
IS_FREE = false
}
}
MotionEvent.ACTION_MOVE -> {
if (leftCanMove) {
val tempX = leftCirX + x - leftOffsetX
if (tempX >= smallCirRadius && tempX <= width / 2 - smallCirShadowRadius) {
leftCirX += x - leftOffsetX
if (leftCirX > width / 2 - smallCirShadowRadius) {//限定最大值
leftCirX = width / 2f - smallCirShadowRadius
}
if (leftCirX < smallCirRadius) {//限定最小值
leftCirX = smallCirRadius
}
}
leftOffsetX = x
}
if (rightCanMove) {
val tempX = rightCirX + x - rightOffsetX
if (tempX >= width / 2 + smallCirShadowRadius && tempX <= width - smallCirRadius) {
rightCirX += x - rightOffsetX
if (rightCirX < width / 2f + smallCirShadowRadius) {//限定最小值
rightCirX = width / 2f + smallCirShadowRadius
}
if (rightCirX > width - smallCirRadius) {//限定最大值
rightCirX = width - smallCirRadius
}
}
rightOffsetX = x
}
}
MotionEvent.ACTION_UP -> {
IS_FREE = true
if (leftCanMove && leftCirX >= width / 2 - changedOffset - smallCirShadowRadius) {
actionIsLeft = true
leftChangedListener?.onClick(this@SlideSwitchView)
}
if (rightCanMove && rightCirX <= width / 2 + changedOffset + smallCirShadowRadius) {
actionIsLeft = false
rightChangedListener?.onClick(this@SlideSwitchView)
}
leftCanMove = false
rightCanMove = false
}
else -> {
return super.onTouchEvent(event)
}
}
}
return true
}
//恢复原样
fun reSet() {
if (actionIsLeft) {
alphaLeft = 0
} else {
alphaRight = 0
}
leftCirX = smallCirRadius
rightCirX = width - smallCirRadius
}
fun Context.dp2px(dp: Int) = (resources.displayMetrics.density * dp + .5f).toInt()
fun Context.sp2px(sp: Int) = resources.displayMetrics.scaledDensity * sp + .5f
}
2、xml调用:
<com.view.SlideSwitchView
android:id="@+id/ssv"
android:layout_width="match_parent"
android:layout_height="120dp"
android:layout_marginTop="100dp"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"
android:layerType="software" />
3、方法使用:
slideSwitchView.leftChangedListener = View.OnClickListener {
Log.d("caowj", "左侧拖拽完成触发事件")
showToast("左拖拽")
// nViewDataBinding.ssv.setLeftImg()
nViewDataBinding.ssv.leftText = "下班"
nViewDataBinding.ssv.reSet()
}
slideSwitchView.rightChangedListener = View.OnClickListener {
Log.d("caowj", "右侧拖拽完成触发事件")
// nViewDataBinding.ssv.setRightImg()
nViewDataBinding.ssv.rightText = "离职"
showToast("右拖拽")
nViewDataBinding.ssv.reSet()
}
本文介绍如何在Android中创建一个可拖动按钮来接听电话的效果,包括SlideSwitchView的实现,XML布局调用及方法使用,同时加入了动画效果。

7918

被折叠的 条评论
为什么被折叠?



