一、概述
转眼到2022年了,回想第一次在csdn上写东西是2017年,距现在已过去5年。这5年之间,身边的人有的结婚了,有的生小孩了,有的买房了,有的升级做管理不再写代码了。而我,与这些都没有关系,还是一个菜,只不过从一个小菜变成了一个老菜,功不成名不就。 既然已经是这样,多说和感慨也无法改变,不如做好手下的事吧!
去年在项目中写过一个浮在页面上随手指拖动的View,因为需要在很多页面用到,所以封装了一下,感觉还是有点必要记录一下,方便自己日后再次使用,也希望能给有类似需求的你提供参考,当然这并不是很难实现。
效果是这样: 1、浮在页面上,可以随手指拖动; 2、手指释放后,缓慢平移到距离最近的页面边缘; 3、页面上滑时,会缩到页面边缘里面;下滑时,会伸出来。 看效果图:
二、代码实现
FloatDragView不是继承View,要显示的floatView通过构造方法中传入。里面封装了实现逻辑: 1、先将floatView添加到父View中,根据父View的类型,设置layoutParams,放置在右下角; 2、给floatView设置OnTouchListener,手指移动时,计算出x、y的位移,再检查边界后(不能滑出父View),改变translationX、translationY实现移动(原本使用offsetLeftAndRight、offsetTopAndBottom,但发现有些许问题);手指释放时,计算距离屏幕最近的水平值,通过动画移动到边缘; 3、监听可滑动的view,滑动时移动到相应位置,达到缩起和伸出效果。
下面上代码:
class FloatDragView(private var floatView: View?) {
private val touchSlop by lazy(LazyThreadSafetyMode.NONE) {
val context = floatView?.context
if (context != null)
ViewConfiguration.get(context).scaledTouchSlop
else
0
}
private var parentView: ViewGroup? = null
private var scrollView: View? = null
private var clickAction: ((View) -> Unit)? = null
private var isAnimatorRunning = false
private var isCollapse = false
private var collapseWidth = 0f
private var rightMargin = 0
private var isDragStatus = false
private var isOnTheRight = true
fun attach(
parentView: ViewGroup, canScrollView: View? = null,
clickAction: ((View) -> Unit)? = null
) {
this.scrollView = canScrollView
this.parentView = parentView
this.clickAction = clickAction
addFloatView()
}
fun removeFloatView() {
parentView?.removeView(floatView)
floatView = null
}
fun setVisibility(visibility: Int) {
floatView?.visibility = visibility
}
private fun addFloatView() {
val contentView = parentView ?: return
val floatView = floatView ?: return
if (floatView.parent != null) {
contentView.removeView(floatView)
}
val context = contentView.context
floatView.setOnClickListener {
if (isCollapse) {
floatOut()
} else {
clickAction?.invoke(it)
}
}
setupDrag(floatView)
val resource = context.resources
val width = resource.getDimensionPixelSize(R.dimen.dp_56)
val height = resource.getDimensionPixelSize(R.dimen.dp_44)
rightMargin = resource.getDimensionPixelSize(R.dimen.dp_8)
val bMargin = resource.getDimensionPixelSize(R.dimen.dp_70)
collapseWidth = rightMargin + width * 0.6f
val params = when (contentView) {
is FrameLayout -> {
FrameLayout.LayoutParams(width, height).apply {
gravity = Gravity.BOTTOM or Gravity.END
}
}
is RelativeLayout -> {
RelativeLayout.LayoutParams(width, height).apply {
addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
addRule(RelativeLayout.ALIGN_PARENT_RIGHT)
}
}
is ConstraintLayout -> {
ConstraintLayout.LayoutParams(width, height).apply {
endToEnd = ConstraintLayout.LayoutParams.PARENT_ID
bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID
}
}
else -> {
ViewGroup.MarginLayoutParams(width, height)
}
}
params.rightMargin = rightMargin
params.bottomMargin = bMargin
contentView.addView(floatView, params)
val canScrollView = scrollView ?: return
when (canScrollView) {
is ScrollView -> {
setOnTouchListener(canScrollView)
}
is NestedScrollView -> {
setOnScrollChangeListener(canScrollView)
}
is RecyclerView -> {
addOnScrollListener(canScrollView)
}
is AppBarLayout -> {
addOnOffsetChangedListener(canScrollView)
}
}
}
@SuppressLint("ClickableViewAccessibility")
private fun setupDrag(view: View) {
view.setOnTouchListener(object : View.OnTouchListener {
private var lastX = 0f
private var lastY = 0f
override fun onTouch(v: View, event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
lastX = event.x
lastY = event.y
}
MotionEvent.ACTION_MOVE -> {
if (isCollapse) return false
val width = parentView?.width ?: return false
val height = parentView?.height ?: return false
val x = event.x
val y = event.y
if (!isDragStatus) {
if (abs(x - lastX) >= touchSlop || abs(y - lastY) >= touchSlop) {
isDragStatus = true
} else {
return false
}
}
var tX = v.translationX + (x - lastX)
var tY = v.translationY + (y - lastY)
if ((v.top + tY) < 0) {
tY += 0 - (v.top + tY)
}
if ((v.left + tX) < 0) {
tX += 0 - (v.left + tX)
}
if ((v.bottom + tY) > height) {
tY -= (v.bottom + tY) - height
}
if ((v.right + tX) > width) {
tX -= (v.right + tX) - width
}
v.translationX = tX
v.translationY = tY
return true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
if (isDragStatus) {
isDragStatus = false
val tx = v.translationX
val parentCenterX = (parentView?.width ?: return false) / 2
val myCenterX = v.left + tx + v.width / 2
if (myCenterX > parentCenterX) {
isOnTheRight = true
startAnimator(tx, 0f)
} else {
isOnTheRight = false
val to = tx - (v.left + tx) + rightMargin
startAnimator(tx, to)
}
return true
}
}
}
return false
}
})
}
@SuppressLint("ClickableViewAccessibility")
private fun setOnTouchListener(view: View) {
view.setOnTouchListener(object : View.OnTouchListener {
var lastScrollY = 0
override fun onTouch(v: View, event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_MOVE) {
val scrollY = view.scrollY
val dy = scrollY - lastScrollY
if (!isCollapse && dy >= touchSlop) {
collapse()
} else if (isCollapse && dy <= -touchSlop) {
floatOut()
}
lastScrollY = view.scrollY
}
return false
}
})
}
private fun setOnScrollChangeListener(nestedScrollView: NestedScrollView) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
nestedScrollView.setOnScrollChangeListener(View.OnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY ->
val dy = scrollY - oldScrollY
if (!isCollapse && dy >= touchSlop) {
collapse()
} else if (isCollapse && dy <= -touchSlop) {
floatOut()
}
})
} else {
setOnTouchListener(nestedScrollView)
nestedScrollView.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY ->
val dy = scrollY - oldScrollY
if (!isCollapse && dy >= touchSlop) {
collapse()
} else if (isCollapse && dy <= -touchSlop) {
floatOut()
}
})
}
}
private fun addOnScrollListener(recyclerView: RecyclerView) {
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (!isCollapse && dy >= touchSlop) {
collapse()
} else if (isCollapse && dy <= -touchSlop) {
floatOut()
}
}
})
}
private fun addOnOffsetChangedListener(appBarLayout: AppBarLayout) {
appBarLayout.addOnOffsetChangedListener(object : AppBarLayout.OnOffsetChangedListener {
var lastOffset = 0
override fun onOffsetChanged(appBarLayout: AppBarLayout?, verticalOffset: Int) {
if (lastOffset - verticalOffset > touchSlop / 2) {
collapse()
} else if (verticalOffset - lastOffset > touchSlop / 2) {
floatOut()
}
lastOffset = verticalOffset
}
})
}
private fun collapse() {
if (isDragStatus || isCollapse) return
isCollapse = true
if (isOnTheRight) {
startAnimator(0f, collapseWidth)
} else {
val tx = floatView?.translationX ?: return
startAnimator(tx, tx - collapseWidth)
}
}
private fun floatOut() {
if (isDragStatus || !isCollapse) return
isCollapse = false
if (isOnTheRight) {
startAnimator(collapseWidth, 0f)
} else {
val tx = floatView?.translationX ?: return
startAnimator(tx, tx + collapseWidth)
}
}
private fun startAnimator(form: Float, to: Float) {
if (isAnimatorRunning) return
isAnimatorRunning = true
val view = floatView ?: return
val animator = ObjectAnimator.ofFloat(view, "translationX", form, to)
animator.duration = 180
animator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
isAnimatorRunning = false
}
})
animator.start()
}
}
三、使用
创建FloatDragView对象,传入一个view,这个view是显示出来的东西; 调用attach方法,需要传入三个参数,一个是父View(支持FrameLayout、RelativeLayout、ConstraintLayout,如果传入的父View是LinearLayout那是没办法浮在页面上面的);第二个参数是可滑动的View(支持ScrollView、NestedScrollView、RecyclerView、AppBarLayout),传入后会根据它的上下滑动进行缩起和伸出。可以不传,便没有这个效果;第三个参数是个lambda,是点击事件的回调。
val contentView = findViewById<ViewGroup>(R.id.contentView)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.setHasFixedSize(true)
recyclerView.itemAnimator = null
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = RvAdapter()
val iv = ImageView(this)
iv.setImageResource(R.mipmap.red_packget)
FloatDragView(iv).attach(contentView, recyclerView) {
Toast.makeText(this, "click", Toast.LENGTH_SHORT).show()
}
|