最近刷抖音看视频时,对一个视频某个位置比较感兴趣,采用双指放大查看细节,然后还可以随意滑动到任何位置,比较感兴趣,决定自己来实现此效果;
分析效果: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/topprivate var originalXY: IntArray? = null// 触摸时 双指中间的点 / 双指距离private var originalTwoPointerCenter: Point? = nullprivate var originalDistance: Int = 0// 移动时 双指距离 缩放比例private var moveDistance: Int = 0private var moveScale: Float = 0.0f;// 双指移动距离的增量比(用于计算缩放比、背景颜色)private var moveDistanceIncrement: Float = 0.0f// 缩放的Viewprivate var scaleableView: View? = null// 缩放的View原LayoutParamsprivate var viewLayoutParams: ViewGroup.LayoutParams? = null// 缩放的View,在dialog中的LayoutParamsprivate var dialogFrameLayoutParams: FrameLayout.LayoutParams? = null// 用于缩放的dialogprivate var dialog: ScaleDialog? = null// 缩放的动画状态private var isDismissAnimating: Boolean = false//监听回调private var mListener: OnVideoZoomListener? = nullprivate var mOneOrTwoListener: OnVideoZoomOneOrTwoPointListener? = nullfun 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!!.layoutParamsdialogFrameLayoutParams =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 + moveDistanceIncrementmoveScale = 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 = 0fpivotY = 0fscaleX = moveScalescaleY = 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;