使用场景及效果
自定义一个可拖拽的View,常见于游戏相关的场景,例如现在一些小众品牌游戏助手的"一键连招"功能。我们用100行代码实现一个可拖动的View,且拖动结束后固定View的位置。
效果如下
实现可拖拽的小球
需求是当我们拖拽时小球跟手运动,当我们抬手时小球停在抬手的位置,如果我们刷新当前页面(例如页面中某View从GONE到VISIBLE)小球仍然停留在抬手的位置。
step1 实现可拖拽的小球,
代码很简单,全文如下
class FloatDragView : FrameLayout, View.OnTouchListener {
private var mViewWidth = 0
private var mViewHeight = 0
private var mDownX = 0F
private var mDownY = 0F
private var isMove = false
constructor(context: Context) : this(context, null)
constructor(context: Context, attributeSet: AttributeSet?) : this(context, attributeSet, 0)
constructor(context: Context, attributeSet: AttributeSet?, defStyle: Int) : super(
context,
attributeSet,
defStyle
) {
initView()
}
private fun initView() {
val lp = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
layoutParams = lp
val childView = getChildView()
addView(childView)
setOnTouchListener(this)
post {
// 获取一下view宽高备用
mViewWidth = this.width
mViewHeight = this.height
}
}
override fun onTouch(v: View, event: MotionEvent): Boolean {
val x = event.x
val y = event.y
when (event.action) {
MotionEvent.ACTION_DOWN -> {
mDownX = event.x
mDownY = event.y
}
MotionEvent.ACTION_MOVE -> {
isMove = true
//上下偏移,传入值>0 向下偏移,传入值<0向上偏移,会触发重绘
offsetTopAndBottom((y - mDownY).toInt())
offsetLeftAndRight((x - mDownX).toInt())
}
MotionEvent.ACTION_UP -> {
if (isMove) {
//do something
}
isMove = false
}
}
return true
}
private fun getChildView(): View {
val imageView = ShapeableImageView(context)
imageView.setImageResource(R.drawable.icon_float)
imageView.post {
val radius = imageView.width / 2f
imageView.shapeAppearanceModel =
ShapeAppearanceModel().toBuilder().setAllCornerSizes(radius).build()
}
return imageView
}
}
于是我们可以看到如下效果
图怎么这么大:(
拖拽跟手实现了,可为啥一点击刷新(按钮下方的TextView从GONE变VISIBLE)小球又回到了初始位置?因为我们只是调用API offsetTopAndBottom移动了小球的位置,小球实际的LayoutParams参数没变,所以View树一刷新小球又回到初始位置了。
所以需要记录小球初始的位置坐标(绝对坐标or相对坐标都可以,我记录绝对坐标) -> 然后在抬手后获取当前坐标,计算出小球移动的距离 -> 回调出小球移动的距离 -> 业务层根据小球移动的距离固定小球的当前位置。
step2:计算回调小球移动的距离,直接上小球完整代码
class FloatDragView : FrameLayout, View.OnTouchListener {
private var mViewWidth = 0
private var mViewHeight = 0
private var mDownX = 0F
private var mDownY = 0F
private var isMove = false
private var mStartX = 0
private var mStartY = 0
var mLocationListener: LocationListener? = null
constructor(context: Context) : this(context, null)
constructor(context: Context, attributeSet: AttributeSet?) : this(context, attributeSet, 0)
constructor(context: Context, attributeSet: AttributeSet?, defStyle: Int) : super(
context,
attributeSet,
defStyle
) {
initView()
}
private fun initView() {
val lp = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
layoutParams = lp
val childView = getChildView()
addView(childView)
setOnTouchListener(this)
post {
// 获取一下view宽高备用
mViewWidth = this.width
mViewHeight = this.height
// 获取初始绝对坐标
initStartLocation()
}
}
private fun initStartLocation() {
val locations = getAbsLocation()
mStartX = locations[0]
mStartY = locations[1]
}
override fun onTouch(v: View, event: MotionEvent): Boolean {
val x = event.x
val y = event.y
when (event.action) {
MotionEvent.ACTION_DOWN -> {
mDownX = event.x
mDownY = event.y
}
MotionEvent.ACTION_MOVE -> {
isMove = true
//上下偏移,传入值>0 向下偏移,传入值<0向上偏移,会触发重绘
offsetTopAndBottom((y - mDownY).toInt())
offsetLeftAndRight((x - mDownX).toInt())
}
MotionEvent.ACTION_UP -> {
if (isMove) {
//do something
}
isMove = false
val locations = getAbsLocation()
mLocationListener?.onLocation(
locations[0] - mStartX,
locations[1] - mStartY
)
}
}
return true
}
private fun getChildView(): View {
val imageView = ShapeableImageView(context)
imageView.setImageResource(R.drawable.icon_float)
imageView.post {
val radius = imageView.width / 2f
imageView.shapeAppearanceModel =
ShapeAppearanceModel().toBuilder().setAllCornerSizes(radius).build()
}
return imageView
}
private fun getAbsLocation(): IntArray {
val locations = IntArray(2)
getLocationOnScreen(locations)
return locations
}
interface LocationListener {
/**
* 回调当前移动距离
* @param x:X轴移动距离
* @param y:Y轴移动距离
*/
fun onLocation(x: Int, y: Int)
}
}
step3 业务层调用
业务层需要在小球初始化时记录LayouParams信息,然后抬手时根据小球回调的移动距离固定其位置。代码如下,在onCreate生命周期中调用
/**拖动结束后 固定小球当前的layout,防止刷新UI后小球回到初始位置*/
private fun setDragViewLayout() {
//先记录小球的位置
val params = binding.dragView.layoutParams as ConstraintLayout.LayoutParams
marginStart = params.marginStart
marginTop = params.topMargin
//再根据小球回调移动距离固定其位置
binding.dragView.mLocationListener = object : FloatDragView.LocationListener {
override fun onLocation(x: Int, y: Int) {
params.marginStart = marginStart + x
params.topMargin = marginTop + y
binding.dragView.layoutParams = params
}
}
}
代码写完了,如果看了有帮助,帮忙点个赞呀