当前位置:网站首页>带你造轮子,自定义一个随意拖拽可吸边的悬浮View组件
带你造轮子,自定义一个随意拖拽可吸边的悬浮View组件
2022-08-03 23:54:00 【InfoQ】
1、效果

2、前言
3、功能拆解

4、功能实现
4.1、基础实现
4.1.1、自定义view类
class FloatView : FrameLayout {
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 imageView = ShapeableImageView(context)
imageView.setImageResource(R.mipmap.ic_avatar)
addView(imageView)
}
}4.1.2、添加到window
mBinding.btnAddFloat.setOnClickListener {
val contentView = this.window.decorView as FrameLayout
contentView.addView(FloatView(this))
}
val contentView = this.window.decorView.findViewById(android.R.id.content) as FrameLayout
contentView.addView(FloatView(this))

4.1.3、视图层级关系

4.2、拖拽
4.2.1、View.OnTouchListener
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 -> {
offsetTopAndBottom((y - mDownY).toInt())
offsetLeftAndRight((x - mDownX).toInt())
}
MotionEvent.ACTION_UP -> {
}
}
return true
}- MotionEvent.ACTION_DOWN 手指按下
- MotionEvent.ACTION_MOVE 手指滑动
- MotionEvent.ACTION_UP 手指抬起

4.2.2、动态修改view坐标
- view.layout()
- view.setX/view.setY
- view.setTranslationX/view.setTranslationY
- layoutParams.topMargin...
- offsetTopAndBottom/offsetLeftAndRight
4.2.3、view坐标系

4.3、吸边
- 上下吸边
- 左右吸边
4.3.1、上下吸边
1.上半屏:
1.1.滑动距离<半屏=吸顶
1.2.滑动距离>半屏=吸底
2.下半屏:
2.1.滑动距离<半屏=吸底
2.2.滑动距离>半屏=吸顶

private fun adsorbTopAndBottom(event: MotionEvent) {
if (isOriginalFromTop()) {
// 上半屏
val centerY = mViewHeight / 2 + abs(event.rawY - mFirstY)
if (centerY < getScreenHeight() / 2) {
//滑动距离<半屏=吸顶
val topY = 0f + mToolBarHeight
animate().setInterpolator(DecelerateInterpolator()).setDuration(300).y(topY).start()
} else {
//滑动距离>半屏=吸底
val bottomY = getContentHeight() - mViewHeight
animate().setInterpolator(DecelerateInterpolator()).setDuration(300).y(bottomY.toFloat()).start()
}
} else {
// 下半屏
val centerY = mViewHeight / 2 + abs(event.rawY - mFirstY)
if (centerY < getScreenHeight() / 2) {
//滑动距离<半屏=吸底
val bottomY = getContentHeight() - mViewHeight
animate().setInterpolator(DecelerateInterpolator()).setDuration(300).y(bottomY.toFloat()).start()
} else {
//滑动距离>半屏=吸顶
val topY = 0f + mToolBarHeight
animate().setInterpolator(DecelerateInterpolator()).setDuration(300).y(topY).start()
}
}
}4.3.2、左右吸边
1.左半屏:
1.1.滑动距离<半屏=吸左
1.2.滑动距离>半屏=吸右
2.右半屏:
2.1.滑动距离<半屏=吸右
2.2.滑动距离>半屏=吸左
private fun adsorbLeftAndRight(event: MotionEvent) {
if (isOriginalFromLeft()) {
// 左半屏
val centerX = mViewWidth / 2 + abs(event.rawX - mFirstX)
if (centerX < getScreenWidth() / 2) {
//滑动距离<半屏=吸左
val leftX = 0f
animate().setInterpolator(DecelerateInterpolator()).setDuration(300).x(leftX).start()
} else {
//滑动距离<半屏=吸右
val rightX = getScreenWidth() - mViewWidth
animate().setInterpolator(DecelerateInterpolator()).setDuration(300).x(rightX.toFloat()).start()
}
} else {
// 右半屏
val centerX = mViewWidth / 2 + abs(event.rawX - mFirstX)
if (centerX < getScreenWidth() / 2) {
//滑动距离<半屏=吸右
val rightX = getScreenWidth() - mViewWidth
animate().setInterpolator(DecelerateInterpolator()).setDuration(300).x(rightX.toFloat()).start()
} else {
//滑动距离<半屏=吸左
val leftX = 0f
animate().setInterpolator(DecelerateInterpolator()).setDuration(300).x(leftX).start()
}
}
}5、进阶封装
5.1、View封装
5.1.1、BaseFloatView
/**
* 获取子view
*/
protected abstract fun getChildView(): View
/**
* 是否可以拖拽
*/
protected abstract fun getIsCanDrag(): Boolean
/**
* 吸边的方式
*/
protected abstract fun getAdsorbType(): Int5.1.2、子view
class AvatarFloatView(context: Context) : BaseFloatView(context) {
override fun getChildView(): View {
val imageView = ShapeableImageView(context)
imageView.setImageResource(R.mipmap.ic_avatar)
return imageView
}
override fun getIsCanDrag(): Boolean {
return true
}
override fun getAdsorbType(): Int {
return ADSORB_VERTICAL
}
}5.2、调用封装
5.2.1、管理类
private fun addLifecycle(activity: ComponentActivity?) {
activity?.lifecycle?.addObserver(mLifecycleEventObserver)
}
private var mLifecycleEventObserver = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) {
hide()
}
}
fun hide() {
if (::mContentView.isInitialized && mFloatView != null && mContentView.contains(mFloatView!!)) {
mContentView.removeView(mFloatView)
}
mFloatView?.release()
mFloatView = null
mActivity?.lifecycle?.removeObserver(mLifecycleEventObserver)
mActivity = null
}- 添加生命周期的监听
- 在ON_DESTROY的时候处理回收逻辑
5.2.2、FloatManager完整代码
@SuppressLint("StaticFieldLeak")
object FloatManager {
private lateinit var mContentView: FrameLayout
private var mActivity: ComponentActivity? = null
private var mFloatView: BaseFloatView? = null
fun with(activity: ComponentActivity): FloatManager {
mContentView = activity.window.decorView.findViewById(android.R.id.content) as FrameLayout
mActivity = activity
addLifecycle(mActivity)
return this
}
fun add(floatView: BaseFloatView): FloatManager {
if (::mContentView.isInitialized && mContentView.contains(floatView)) {
mContentView.removeView(floatView)
}
mFloatView = floatView
return this
}
fun setClick(listener: BaseFloatView.OnFloatClickListener): FloatManager {
mFloatView?.setOnFloatClickListener(listener)
return this
}
fun show() {
checkParams()
mContentView.addView(mFloatView)
}
private fun checkParams() {
if (mActivity == null) {
throw NullPointerException("You must set the 'Activity' params before the show()")
}
if (mFloatView == null) {
throw NullPointerException("You must set the 'FloatView' params before the show()")
}
}
private fun addLifecycle(activity: ComponentActivity?) {
activity?.lifecycle?.addObserver(mLifecycleEventObserver)
}
private var mLifecycleEventObserver = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) {
hide()
}
}
fun hide() {
if (::mContentView.isInitialized && mFloatView != null && mContentView.contains(mFloatView!!)) {
mContentView.removeView(mFloatView)
}
mFloatView?.release()
mFloatView = null
mActivity?.lifecycle?.removeObserver(mLifecycleEventObserver)
mActivity = null
}
}5.2.3、调用方式
- 显示
FloatManager.with(this).add(AvatarFloatView(this)).show()- 隐藏
FloatManager.hide()- 带点击事件
FloatManager.with(this).add(AvatarFloatView(this))
.setClick(object : BaseFloatView.OnFloatClickListener {
override fun onClick(view: View) {
Toast.makeText([email protected], "click", Toast.LENGTH_SHORT).show()
}
})
.show()6、Github
7、最后
边栏推荐
猜你喜欢

Jar a key generation document database

The world's first mass production, with the most fixed points!How does this AVP Tier1 lead?

Prometheus监控Harbor(二进制版)

伦敦银最新均线分析系统怎么操作?

Justin Sun was invited to attend the 36氪 Yuan Universe Summit and delivered a keynote speech

Talking about the future development direction of my country's industrial parks

ping数据包中的进程号

绕任意轴旋转矩阵推导

Zilliz 2023 Fall Campus Recruitment Officially Launched!

用两个栈模拟队列
随机推荐
Unity2021 releases WebGL fog effect disappearing problem
20年将投资美国约2000亿美元,三星电子财大气粗的样子真好看
IELTS essay writing template
学习笔记 | uiautomation(如何)实现自动化
P1996 约瑟夫问题
北京电竞元宇宙论坛活动顺利召开
Three.js入门详解
用两个栈模拟队列
JS获得URL超链接的参数值
做项目一定用得到的NLP资源【分类版】
MPLS综合实验
Free自由协议系统开发
BPF 可移植性和 CO-RE(一次编译,到处运行)
SPOJ 2774 Longest Common Substring(两串求公共子串 SAM)
智能座舱的「交互设计」大战
The Chinese Valentine's Day event is romantically launched, don't let the Internet slow down and miss the dark time
3D Semantic Segmentation - 2DPASS
【杂项】通过Excel为字符串产生条码
查看CUDA、pytorch等的版本号
Creo 9.0二维草图的诊断:重叠几何