Android:实现一个全屏拖拽、自动贴边半隐藏的自定义View
大家好,我是似曾相识2022。不喜欢唱跳篮球,但对杰伦的Rap却情有独钟。
今天给大家带来一个可全屏拖拽,手指离开屏幕后自动贴边,隔一定时间后自动半隐藏的这么一个效果。话不多说直接上效果图:
看到这个效果是不是感觉很熟悉?没错,很多商业APP首页都带一个小助手的图标,使用的时候点击它就自动弹出,不使用的时候自动贴边隐藏,当然也是可以随意全屏拖拽,为的是防止遮挡一些关键位置的信息,影响用户体验。接下来咱们就来一步步实现它!
要实现上图效果咱们得罗列所有的功能点:
- 自定义View,这里要显示图片所以继承自ImageView或其子类即可
- 监听屏幕滑动事件,记录和计算当前视图的位置信息
- 动画效果,很明显使用平移动画
- 圆角图片和描边,使用第三方ImageView即可
为了解决小圆球这个图标的问题咱们自定View时直接继承自第三方RoundedImageView,一举两得直接解决了第一和第四步。咱们把焦点聚焦到第二三部分,这也是最为复杂的部分。
之前文章 Android:自定义View实现图片缩放及坐标的计算(上) 中有写到监听界面各类手势可以使用GestureDetector,这里咱们就不采用重写onTouchEvent方法然后再里面监听各类ACTION_UP、ACTION_DOWN、ACTION_MOVE事件的模式来写了。但还是需要重写onTouchEvent方法将GestureDetector的处理结果返回给它即可:
override fun onTouchEvent(event: MotionEvent): Boolean {
return gestureDetector.onTouchEvent(event)
}
接下来只需在GestureDetector入参的GestureDetector.SimpleOnGestureListener监听中执行对应的操作:
首先需要在onDown方法中记录最后点击屏幕的位置信息lastX、lastY,这里备份一份点击时的位置信息moveX、moveY,用于后续逻辑判断。
override fun onDown(e: MotionEvent): Boolean {
lastX = e.rawX.toInt()
lastY = e.rawY.toInt()
moveX = lastX
moveY = lastY
return true
}
在onScroll中需要不停修改自定义视图的位置,所以我们需要计算出需要移动位置的信息。通过当前实时滑动点的信息和最后记录的点信息计算出滑动距离,再重新计算当前视图的上下左右位置,最后咱们采取layout() 方式进行位置设置。
override fun onScroll(
e1: MotionEvent,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
//获取当前实时点信息
val rawX = e2.rawX.toInt()
val rawY = e2.rawY.toInt()
//变化量
dX = rawX - lastX
dY = rawY - lastY
//获取最新的视图位置
var left = left + dX
var right = right + dX
var top = top + dY
var bottom = bottom + dY
//添加限制范围,上下左右不能超出屏幕范围
if (left < 0) {
left = 0
right = left + width
}
if (right > windowWith) {
right = windowWith
left = right - width
}
if (top < 40) {
top = 40
bottom = top + height
}
if (bottom > windowHight) {
bottom = windowHight
top = bottom - height
}
//更新当前视图位置
layout(left, top, right, bottom)
//更新最后屏幕点信息
lastX = rawX
lastY = rawY
return true
}
到此,咱们已经实现了可全屏拖拽的效果了:
现在只差最后一步,通过位置信息判断图标该往哪边贴边,以及移动距离的计算。
由于GestureDetector没有抬起监听,所以逻辑咱们还是得在onTouchEvent方法中通过监听ACTION_UP的动作进行操作。判断该往哪边贴边很简单,如果最后松开的位置X坐标的超过屏幕一半就往右贴,反之往左。动画咱们还是使用ValueAnimator,因为我们移动也是用layout() 方法进行操作。
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_UP -> {
val x = event.rawX
val y = event.rawY
//抬起点和最后一次按下点x、y距离大于视图宽的一半才执行
if (abs(x - moveX) > width / 2 || abs(y - moveY) > width / 2) {
val isRight = x > windowWith / 2
//贴边
startAnimator(isRight, windowWith - width, 0)
//隔1.5秒收边
postDelayed({
startAnimator(isRight, windowWith - width * 2 / 3, -width / 3)
}, 1500)
}
return true
}
}
return gestureDetector.onTouchEvent(event)
}
//属性动画执行
private fun startAnimator(isRight: Boolean, rightValue: Int, leftValue: Int) {
ValueAnimator.ofInt(
left,
if (isRight) rightValue else leftValue
).apply {
addUpdateListener { animation ->
val value = animation.animatedValue as Int
//根据监听值不断改变当前视图位置
layout(value, top, value + width, bottom)
}
//插值器 先快后慢
interpolator = AccelerateDecelerateInterpolator()
duration = 600
start()
}
}
这里使用了两次动画,第一次根据计算得出的方向进行贴边平移,隔了1.5秒后再进行隐藏的操作。到此我们的所有功能全部都实现了接下来总结几点:
- 自定义View时尽量选择最接近目标功能的View进行继承
- 屏幕事件监听除了重写onTouchEvent进行动作监听的方式还有GestureDetector、ScaleGestureDetector等方式
- 重写了onTouchEvent方法后需要注意其返回值,如果都返回false的情况该视图的点击事件有可能会被父View或其他设有监听事件控件所消费,导致滑动监听不被触发。
以上便是实现一个全屏拖拽、自动贴边半隐藏的自定义View的所有内容,希望能给大家带来帮助!
来源:juejin.cn/post/7278496260477796389