最近刷抖音看视频时,对一个视频某个位置比较感兴趣,采用双指放大查看细节,然后还可以随意滑动到任何位置,比较感兴趣,决定自己来实现此效果;
分析效果:ViewPager左右滑动,视频列表上下滑动+下拉刷新,双指进行缩放操作计算移动坐标来平移view,双指到单指也可以进行平移
问题评估:viewpager左右滑动和列表左右滑动冲突问题,单指滑动滑出边界和下拉刷新控件手势冲突;
首先双指缩放和单指平移我想到的是,使用ScaleGestureDetector和GestureDetector,这两个一个是监听双指,一个是单指;看下了ScaleGestureDetector.OnScaleGestureListener的三个重写方法:
onScaleonScaleBeginonScaleEnd
双指放下,走的依次是onScaleBegin->onScale->onScaleEnd,只有ScaleBegin返回true时才会调用onScale和onScaleEnd,发现只要双指抬起来走的是onScaleEnd,因为我需要监听up事件才要还原View,发现不太适用;
于是决定换一种思路,是不是可以在双指事件拿到之后对一个弹窗处理呢?如果只有单指Action_down事件,就把事件交给下拉刷新控件处理,双指Action_pointer_down时,弹出弹窗,对弹窗进行缩放,平移也对弹窗进行处理,简单点说就是一个dialog遮罩在页面上,根据双指单指控制展示还是隐藏;
单指:ACTION_DOWN->ACTION_MOVE->ACTION_UP;
多指:ACTION_DOWN->ACTION_POINTER_DOWN->ACTION_MOVE->ACTION_POINTER_UP->ACTION_UP一次完整的触摸事件中,Down和Up都只有一个,Move有若干个,可以为0个。当触摸事件被拦截时,Up可能是0个。View在ViewGroup内,ViewGroup也可以在其他ViewGroup内,这时候把内部的ViewGroup当成View来分析。
自定义控件首先要了解,一种是继承ViewGroup,一种是继承View;
dispatchTouchEventonInterceptTouchEventonTouchEvent
这是我自定义缩放View,获取到手势之后,根据双指、单指事件计算坐标
/**
* 可缩放的Layout
*/
class TouchToScaleLayout(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) {
// 缩放view的初始left/top
private var originalXY: IntArray? = null
// 触摸时 双指中间的点 / 双指距离
private var originalTwoPointerCenter: Point? = null
private var originalDistance: Int = 0
// 移动时 双指距离 缩放比例
private var moveDistance: Int = 0
private var moveScale: Float = 0.0f;
// 双指移动距离的增量比(用于计算缩放比、背景颜色)
private var moveDistanceIncrement: Float = 0.0f
// 缩放的View
private var scaleableView: View? = null
// 缩放的View原LayoutParams
private var viewLayoutParams: ViewGroup.LayoutParams? = null
// 缩放的View,在dialog中的LayoutParams
private var dialogFrameLayoutParams: FrameLayout.LayoutParams? = null
// 用于缩放的dialog
private var dialog: ScaleDialog? = null
// 缩放的动画状态
private var isDismissAnimating: Boolean = false
//监听回调
private var mListener: OnVideoZoomListener? = null
private var mOneOrTwoListener: OnVideoZoomOneOrTwoPointListener? = null
fun setZoomListener(listener: OnVideoZoomListener) {
mListener = listener
}
fun setOneOrTwoPointListener(listener:OnVideoZoomOneOrTwoPointListener){
mOneOrTwoListener=listener
}
override fun onTouchEvent(ev: MotionEvent?): Boolean {
//后续事件将可以继续传递给该view的onTouchEvent()处理
//不想上传递
return true
}
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
if (childCount == 0 && scaleableView == null) return super.dispatchTouchEvent(ev)
when (ev.actionMasked) {
MotionEvent.ACTION_DOWN -> {
Log.e("--------", "ACTION_DOWN")
if (mOneOrTwoListener!=null){
mOneOrTwoListener!!.onOnePoint()
}
//交与系统处理
return super.dispatchTouchEvent(ev)
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
//抬起直接复原下拉上拉
if (mOneOrTwoListener!=null){
mOneOrTwoListener!!.onOnePoint()
}
if (ev.action == MotionEvent.ACTION_UP) {
Log.e("--------", "ACTION_UP")
}
if (ev.action == MotionEvent.ACTION_CANCEL) {
Log.e("--------", "ACTION_CANCEL")
}
//自己处理
requestDisallowInterceptTouchEvent(true)
if (scaleableView != null) {
if (!isDismissAnimating) {
dismissWithAnimator()
}
return true
}
}
MotionEvent.ACTION_POINTER_DOWN -> {
if (mOneOrTwoListener!=null){
mOneOrTwoListener!!.onTwoPoint()
}
Log.e("--------", "ACTION_POINTER_DOWN")
//自己处理
requestDisallowInterceptTouchEvent(true)
if (scaleableView == null && childCount > 0) {
scaleableView = getChildAt(0)
originalXY = IntArray(2)
scaleableView?.getLocationOnScreen(originalXY)
dialog = ScaleDialog(context)
dialog?.show()
viewLayoutParams = scaleableView!!.layoutParams
dialogFrameLayoutParams =
LayoutParams(scaleableView!!.width, scaleableView!!.height).apply {
leftMargin = originalXY!![0]
topMargin = originalXY!![1]
}
postDelayed({
if (scaleableView != null && scaleableView?.parent == this && !isDismissAnimating) {
removeView(scaleableView)
dialog?.addView(scaleableView!!, dialogFrameLayoutParams)
}
}, 80)
}
originalDistance = getDistance(ev)
if (originalTwoPointerCenter == null) {
originalTwoPointerCenter = Point()
}
originalTwoPointerCenter?.x = getTwoPointerCenterX(ev)
originalTwoPointerCenter?.y = getTwoPointerCenterY(ev)
return true
}
MotionEvent.ACTION_MOVE -> {
Log.e("--------", "ACTION_MOVE")
if (scaleableView != null && scaleableView?.parent != this) {
if (ev.pointerCount == 2) {
if (mOneOrTwoListener!=null){
mOneOrTwoListener!!.onTwoPoint()
}
// 双指距离和距离比例
moveDistance = getDistance(ev)
moveDistanceIncrement =
(moveDistance.toFloat() - originalDistance.toFloat()) / originalDistance.toFloat()
// 关键点:
// 1.设置pivotX和pivotY为view左上角,相比View中心点更容易计算缩放后的位移
// 2.位移计算公式 (触摸屏幕时的坐标 * 缩放比 = 缩放后的坐标,当前两指中心点 - 缩放后的坐标 + 触摸屏幕时的leftMargin和topMargin = left和top最终需要的位移)
// leftMargin = 当前两指中心点的x坐标 - 首次触摸屏幕时两指中心点的x坐标 乘以 缩放比 + 首次触摸时的原始leftMargin
// topMargin同上,将x换成y
// 缩放
moveScale = 1 + moveDistanceIncrement
moveScale = max(0.5f, moveScale)
moveScale = min(3.0f, moveScale)
if (moveScale < 1) {
if (mListener != null) {
mListener!!.onScaleEnd(false)
}
} else if (moveScale > 1) {
//手指按下直接设置展示
if (mListener != null) {
mListener!!.onScaleEnd(true)
}
}
scaleableView?.run {
pivotX = 0f
pivotY = 0f
scaleX = moveScale
scaleY = moveScale
}
// 位移
if (originalTwoPointerCenter != null && originalXY != null) {
updateOffset(
(getTwoPointerCenterX(ev) - originalTwoPointerCenter!!.x * moveScale) + originalXY!![0].toFloat(),
(getTwoPointerCenterY(ev) - originalTwoPointerCenter!!.y * moveScale) + originalXY!![1].toFloat()
)
}
// 透明背景
dialog?.setShadowAlpha(max(min(0.8f, moveDistanceIncrement / 1.5f), 0f))
return true
} else if (ev.pointerCount == 1) { //单指移动
updateOffset(
getOnePointerCenterX(ev) - getOnePointerCenterX(ev) * moveScale,
getOnePointerCenterY(ev) - getOnePointerCenterY(ev) * moveScale
)
// 透明背景
dialog?.setShadowAlpha(max(min(0.8f, moveDistanceIncrement / 1.5f), 0f))
return true
}
}
}
}
//事件继续向下分发
return super.dispatchTouchEvent(ev)
}
...
}
如果你使用的有下拉刷新,注意把下拉、上拉进行动态控制,否则在滑动中会系统通知ACTION_CANCEL;