效果图:
特性模仿
- 全屏可视区域滑动检测 (菜单关闭和打开状态, 都支持)
- 内容区域滑动过程中自带阴影遮罩
- 菜单打开状态, 点击阴影区域自动关闭
- 滑动过程中, 视差效果
- 可以嵌套在其他具有滚动特性的View中
实现方法如果使用 ViewDragHelper
那么局限性会很多, 所以这里我采用了最原始的TouchEvent控制.
以下代码, 只贴部分片段, 详细请下载源码
首先监听Touch事件
任何需要处理Touch事件的控件, 都必备回调2个方法onScroll
和onFling
, 缺一个都是不完整的.
onScroll
:用在简单的手指上下,左右移动. 这个方法会回调出, 手指与上一个事件的移动间隔距离
onFling
:用在手指快速的上下,左右移动. 这个方法回调出的是手指与上一个事件之间的移动速度
/*用来检测手指滑动方向*/
protected val orientationGestureDetector = GestureDetectorCompat(context, object : GestureDetector.SimpleOnGestureListener() {
override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean {
//L.e("call: onFling -> \n$e1 \n$e2 \n$velocityX $velocityY")
firstMotionEvent = e1
secondMotionEvent = e2
val absX = Math.abs(velocityX)
val absY = Math.abs(velocityY)
if (absX > flingVelocitySlop || absY > flingVelocitySlop) {
if (absY > absX) {
//竖直方向的Fling操作
onFlingChange(if (velocityY > 0) ORIENTATION.BOTTOM else ORIENTATION.TOP, velocityY)
} else if (absX > absY) {
//水平方向的Fling操作
onFlingChange(if (velocityX > 0) ORIENTATION.RIGHT else ORIENTATION.LEFT, velocityX)
}
}
return true
}
override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean {
//L.e("call: onScroll -> \n$e1 \n$e2 \n$distanceX $distanceY")
firstMotionEvent = e1
secondMotionEvent = e2
val absX = Math.abs(distanceX)
val absY = Math.abs(distanceY)
if (absX > scrollDistanceSlop || absY > scrollDistanceSlop) {
if (absY > absX) {
//竖直方向的Scroll操作
onScrollChange(if (distanceY > 0) ORIENTATION.TOP else ORIENTATION.BOTTOM, distanceY)
} else if (absX > absY) {
//水平方向的Scroll操作
onScrollChange(if (distanceX > 0) ORIENTATION.LEFT else ORIENTATION.RIGHT, distanceX)
}
}
return true
}
})
如何让Layout移动起来?
通常情况下在View中, 我们会想到用View的x,y, 坐标来控制.或者translationX, translationY, 等;
在ViewGroup, 我们可以用ScrollTo, ScrollBy方法, 让布局移动起来.
但是, 我这里用了View.Layout方法, 通过改变View在ViewGroup中的坐标, 达到移动效果.
private fun refreshContentLayout(left: Int) {
if (childCount == 2) {
getChildAt(1).apply {
layout(left, 0, left + this.measuredWidth, this.measuredHeight) //关键点
}
}
}
我们只需要, 不断的传入不同的left
坐标值, 就可以让View移动起来;
那么如何让left
不断变化呢?
很自然的就想到了用动画, 动画虽然也简单, 但是我这里用了一个和自定义View密切相关的另一个必备神器类OverScroller
也许你会觉得OverScroller
是用来滚动View的, 那你就太小看它了. 它其实内部也是动画机制.
如何使用OverScroller让left动起来?
其实, 你只要会用它, 基本上不难, 关键在于…请往下看!
/**开始滚动到某个位置*/
open fun startScrollTo(startX: Int, toX: Int) {
overScroller.startScroll(startX, scrollY, toX - startX, 0, 300)
postInvalidate()
}
与OverScroller
密切关联的方法就是computeScroll
了.其实你把它理解成OverScroller
的执行回调, 可能更好一点.
override fun computeScroll() {
if (overScroller.computeScrollOffset()) {
//scrollTo(overScroller.currX, overScroller.currY) //原本应该是这样的.
val currX = overScroller.currX
if (contentLayoutLeft != currX) {
refreshContentLayout(currX) //但是...投机取巧, 我们用这个...完美的让left动起来了.
}
postInvalidate()
}
}
到此:ViewDragHelper
的拖动功能, 已经模仿出来了.但是, 我们的可扩展性, 可操作比他大100倍.
如何营造视差效果?
对于这个, 其实我们只需要在 更新内容left的时候, 同步更新菜单的left就行了. 只不过left要经过阻尼
处理一下. 效果才逼真.
//单独更新菜单,营造视差滚动
private fun refreshMenuLayout() {
//计算出菜单展开的比例
val fl = contentLayoutLeft.toFloat() / maxMenuWidth
if (fl > 0f && childCount > 0) {
getChildAt(0).apply {
//视差开始时的偏移值
val menuOffsetStart = -maxMenuWidth / 2
val left = menuOffsetStart + (menuOffsetStart.abs() * fl).toInt()
layout(left, 0, left + this.measuredWidth, this.measuredHeight)
}
}
}
如何实现阴影遮罩?
玩过Canvas的应该都知道, 直接绘制一个透明图层就行了. 关键在于, 透明度要跟随菜单的打开程度动态计算
override fun draw(canvas: Canvas) {
super.draw(canvas)
//绘制内容区域的阴影遮盖
if (isMenuClose()) {
} else {
val layoutLeft = contentLayoutLeft
debugPaint.color = Color.BLACK.tranColor((255 * (layoutLeft.toFloat() / maxMenuWidth) * 0.4f /*限制一下值*/).toInt())
debugPaint.style = Paint.Style.FILL_AND_STROKE
maskRect.set(layoutLeft, 0, measuredWidth, measuredHeight)
canvas.drawRect(maskRect, debugPaint)
}
}
也许你还想学习更多, 来我的群吧, 我写代码的能力, 远大于写文章的能力:
联系作者
请使用QQ扫码加群, 小伙伴们在等着你哦!
关注我的公众号, 每天都能一起玩耍哦!