floatwidgetdemo
废话少说,效果图如上。
思路
在onInterceptTouchEvent()判断是否点击在控件上,在长按的情况下拦截后续事件,在onTouchEvent()处理控件移动和 MotionEvent.ACTION_UP事件,手指拿起后执行吸附动画。
1、按下去的时候判断开启线程,执行动画完毕后,将长按标志为true
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
if (inTouchChild(ev.x, ev.y)) {
parent.requestDisallowInterceptTouchEvent(true)
postDelayed(checkForTap, CHECK_DELAY_MILLIS)
}
}
}
//放大按钮并设置长按标志
inner class CheckForTap : Runnable {
override fun run() {
if (!inTouchChild(actionMoveX, actionMoveY)) {
return
}
floatWidget.animate()
.scaleX(MAX_SCALE)
.scaleY(MAX_SCALE)
.setDuration(150)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation)
if (!inTouchChild(actionMoveX, actionMoveY)) {
// 恢复到按压时的位置
upOrCancelEvent(actionDownX, 0)
return
}
isLongPressed = true //动画结束才设置为长按
}
}).start()
}
}
2、在 MotionEvent.ACTION_MOVE时拦截事件,交由onTouchEvent处理
override fun onTouchEvent(event: MotionEvent): Boolean {
if (!touchIntercept) {
return super.onTouchEvent(event)
}
when (event.action) {
MotionEvent.ACTION_MOVE -> {
val offsetX = event.x - actionDownX
val offsetY = event.y - actionDownY
updateLayoutParams(offsetX.toInt(), offsetY.toInt())
}
MotionEvent.ACTION_CANCEL,
MotionEvent.ACTION_UP -> {
upOrCancelEvent(event.x)
}
}
actionDownX = event.x
actionDownY = event.y
return true
}
完整代码如下
package com.example.viewdemo.widget
import android.animation.*
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Rect
import android.os.Build
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.ViewConfiguration
import android.view.animation.DecelerateInterpolator
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.annotation.RequiresApi
import com.example.viewdemo.R
import kotlin.math.abs
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
class FloatWidgetLayout : FrameLayout {
companion object {
private const val CHECK_DELAY_MILLIS = 100L
private const val MAX_SCALE = 1.2f
}
private var actionDownX = 0f
private var actionDownY = 0f
private var actionMoveX = 0f
private var actionMoveY = 0f
private var touchSlop = 0
private var touchIntercept = false //是否拦截
private var isLongPressed = false //是否长按
private var checkForTap = CheckForTap() //
private lateinit var floatWidget: ImageView
private val floatWidgetFrame = Rect() //获取宽高
constructor(context: Context) : super(context) {
initView(context)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
initView(context)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) :
super(context, attrs, defStyleAttr) {
initView(context)
}
private fun initView(context: Context) {
val view = LayoutInflater.from(context).inflate(
R.layout.layout_float_widget, null, true
)
touchSlop = ViewConfiguration.get(context).scaledTouchSlop
floatWidget = view.findViewById(R.id.iv_float)
floatWidget.apply {
isClickable = true
}
addView(view)
}
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
actionDownX = ev.x
actionDownY = ev.y
actionMoveX = ev.x
actionMoveY = ev.y
touchIntercept = false
isLongPressed = false
}
MotionEvent.ACTION_MOVE -> {
actionMoveX = ev.x
actionMoveY = ev.y
}
}
return super.dispatchTouchEvent(ev)
}
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
if (inTouchChild(ev.x, ev.y)) {
parent.requestDisallowInterceptTouchEvent(true)
postDelayed(checkForTap, CHECK_DELAY_MILLIS)
}
}
MotionEvent.ACTION_MOVE -> {
val offsetX = abs(ev.x - actionDownX)
val offsetY = abs(ev.y - actionDownY)
val inTouchChild = inTouchChild(ev.x, ev.y)
val x = offsetX > touchSlop
val y = offsetY > touchSlop
if (isLongPressed && inTouchChild && (x || y)) { //长按而且点击在控件上而且发生了滑动
touchIntercept = true
actionDownX = ev.x
actionDownY = ev.y
return true
}
}
MotionEvent.ACTION_CANCEL,
MotionEvent.ACTION_UP -> {
removeCallbacks(checkForTap)
if (isLongPressed) {
upOrCancelEvent(ev.x)
} else {
upOrCancelEvent(actionDownX, 0)
}
}
}
return super.onInterceptTouchEvent(ev)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
if (!touchIntercept) {
return super.onTouchEvent(event)
}
when (event.action) {
MotionEvent.ACTION_MOVE -> {
val offsetX = event.x - actionDownX
val offsetY = event.y - actionDownY
updateLayoutParams(offsetX.toInt(), offsetY.toInt())
}
MotionEvent.ACTION_CANCEL,
MotionEvent.ACTION_UP -> {
upOrCancelEvent(event.x)
}
}
actionDownX = event.x
actionDownY = event.y
return true
}
inner class CheckForTap : Runnable {
override fun run() {
if (!inTouchChild(actionMoveX, actionMoveY)) {
return
}
floatWidget.animate()
.scaleX(MAX_SCALE)
.scaleY(MAX_SCALE)
.setDuration(150)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation)
if (!inTouchChild(actionMoveX, actionMoveY)) {
// 恢复到按压时的位置
upOrCancelEvent(actionDownX, 0)
return
}
isLongPressed = true //动画结束才设置为长按
}
}).start()
}
}
private fun inTouchChild(x: Float, y: Float): Boolean {
floatWidget.getHitRect(floatWidgetFrame)
return floatWidgetFrame.contains(x.toInt(), y.toInt())
}
private fun upOrCancelEvent(x: Float, animationDuration: Long = 500) {
val targetMarginStart = if (x < width / 2f) 0 else width - floatWidget.width //超过一半靠右,否正靠左
startSpringBackAnimator(targetMarginStart, animationDuration)
}
private fun startSpringBackAnimator(
targetMarginStart: Int,
animationDuration: Long
) {
// 缩放动画
val xScaleAnimation = ObjectAnimator.ofFloat(
floatWidget,
"scaleX", floatWidget.scaleX, 1.0f
)
xScaleAnimation.duration = 150
xScaleAnimation.interpolator = DecelerateInterpolator()
val yScaleAnimation = ObjectAnimator.ofFloat(
floatWidget,
"scaleY", floatWidget.scaleY, 1.0f
)
yScaleAnimation.duration = 150
yScaleAnimation.interpolator = DecelerateInterpolator()
// 吸附动画
val layoutParams = floatWidget.layoutParams as MarginLayoutParams
val xTranslationAnimation = ValueAnimator.ofInt(
layoutParams.marginStart, targetMarginStart
)
xTranslationAnimation.addUpdateListener {
val lp = floatWidget.layoutParams as MarginLayoutParams
lp.marginStart = it.animatedValue as Int
floatWidget.layoutParams = lp
}
xTranslationAnimation.interpolator = DecelerateInterpolator()
xTranslationAnimation.duration = animationDuration
val animatorSet = AnimatorSet()
animatorSet.play(xScaleAnimation).with(yScaleAnimation)
.before(xTranslationAnimation)
animatorSet.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationCancel(animation: Animator?) {
super.onAnimationCancel(animation)
onAnimationEnd(animation)
}
override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation)
}
})
animatorSet.start()
}
/**
* 移动widget
*/
private fun updateLayoutParams(
marginStartOffset: Int,
topMarginOffset: Int
) {
(floatWidget.layoutParams as? MarginLayoutParams)?.let { lp ->
var newTopMargin = lp.topMargin + topMarginOffset
lp.topMargin = newTopMargin
var newMarginStart = lp.marginStart + marginStartOffset
if (newMarginStart < 0) {
newMarginStart = 0
} else if (newMarginStart > width - floatWidget.width) {
newMarginStart = width - floatWidget.width
}
lp.marginStart = newMarginStart
floatWidget.layoutParams = lp
}
}
}