Kotlin实现ScrollView缩放后支持平移拖拽功能
Kotlin实现ScrollView缩放后平移方案
原有实现问题
- 采用
ScaleAnimation配合fillAfter仅做渲染层面的补间动画,没有实际修改视图属性,会导致触摸区域和显示位置错位,平移逻辑无法正常生效 - 缩放值计算逻辑写反,捏合手势的缩放方向和预期不符
GestureDetector仅实现了按下、双击回调,没有处理滑动平移的逻辑- 触摸事件分发存在重复传递问题,容易引发手势冲突
核心修改逻辑
- 放弃补间动画,直接修改ScrollView的
scaleX/scaleY属性实现缩放,保证触摸区域和显示位置一致 - 新增平移偏移量记录,在手势监听器的
onScroll回调中处理放大状态下的平移逻辑 - 增加平移边界校验,避免缩放后的内容被拖出可视范围露出空白
- 修正事件分发逻辑,优先响应缩放手势,缩放未触发时,未放大状态交给ScrollView原生处理垂直滚动,放大状态响应自定义平移
完整实现代码
MainActivity.kt
class MainActivity : AppCompatActivity() { private var mScale = 1f private lateinit var mScaleGestureDetector: ScaleGestureDetector private lateinit var gestureDetector: GestureDetector private lateinit var scrollView: ScrollView // 平移相关参数 private var transX = 0f private var transY = 0f private var scrollViewWidth = 0 private var scrollViewHeight = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) scrollView = findViewById(R.id.scrollView) // 布局完成后获取ScrollView实际宽高,用于计算平移边界 scrollView.doOnLayout { scrollViewWidth = it.width scrollViewHeight = it.height } // 初始化手势识别器 gestureDetector = GestureDetector(this, GestureListener()) mScaleGestureDetector = ScaleGestureDetector(this, object : ScaleGestureDetector.SimpleOnScaleGestureListener() { override fun onScale(detector: ScaleGestureDetector): Boolean { val scaleFactor = detector.scaleFactor // 修正缩放值计算逻辑 mScale *= scaleFactor // 限制缩放范围:最小0.5倍,最大2倍,可根据需求自行调整 mScale = mScale.coerceIn(0.5f, 2f) // 缩放回到1倍时重置平移偏移 if (mScale == 1f) { transX = 0f transY = 0f scrollView.translationX = 0f scrollView.translationY = 0f } // 直接修改视图属性实现缩放,替代补间动画 scrollView.scaleX = mScale scrollView.scaleY = mScale // 缩放后校验平移边界,防止内容错位 checkTranslationBounds() scrollView.translationX = transX scrollView.translationY = transY return true } }) } override fun dispatchTouchEvent(event: MotionEvent?): Boolean { event ?: return super.dispatchTouchEvent(null) // 优先交给缩放手势识别器处理 mScaleGestureDetector.onTouchEvent(event) // 正在缩放时直接消费事件,不触发其他手势 if (mScaleGestureDetector.isInProgress) { return true } // 交给滑动手势识别器处理,未消费的事件交回系统原生处理(保证未放大时ScrollView正常滚动) return if (gestureDetector.onTouchEvent(event)) true else super.dispatchTouchEvent(event) } // 平移边界校验,防止内容拖出可视范围 private fun checkTranslationBounds() { val scaledWidth = scrollViewWidth * mScale val scaledHeight = scrollViewHeight * mScale // 计算当前缩放下允许的最大平移距离 val maxTransX = (scaledWidth - scrollViewWidth) / 2f val maxTransY = (scaledHeight - scrollViewHeight) / 2f // 限制平移范围 transX = transX.coerceIn(-maxTransX, maxTransX) transY = transY.coerceIn(-maxTransY, maxTransY) } private inner class GestureListener : GestureDetector.SimpleOnGestureListener() { override fun onDown(e: MotionEvent): Boolean { return true } override fun onScroll( e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float ): Boolean { // 未放大时不拦截滑动,交给ScrollView原生处理垂直滚动 if (mScale <= 1f) return false // 累加平移偏移 transX -= distanceX transY -= distanceY // 边界校验后应用平移 checkTranslationBounds() scrollView.translationX = transX scrollView.translationY = transY return true } override fun onDoubleTap(e: MotionEvent): Boolean { // 可自行添加双击快速缩放逻辑,比如双击在1倍和2倍之间切换 return true } } }
布局文件activity_main.xml
原有布局无需修改,保持当前实现即可:
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <ScrollView android:id="@+id/scrollView" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <ImageView android:id="@+id/image1" android:layout_width="match_parent" android:layout_height="wrap_content" android:contentDescription="@string/no_image" android:src="@drawable/image1"/> <ImageView android:id="@+id/image2" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="20dp" android:contentDescription="@string/no_image" android:src="@drawable/image2" /> <ImageView android:id="@+id/image3" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="20dp" android:contentDescription="@string/no_image" android:src="@drawable/image3" /> <ImageView android:id="@+id/image4" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="20dp" android:contentDescription="@string/no_image" android:src="@drawable/image4" /> <ImageView android:id="@+id/image5" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="20dp" android:contentDescription="@string/no_image" android:src="@drawable/image5" /> <ImageView android:id="@+id/image6" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="20dp" android:contentDescription="@string/no_image" android:src="@drawable/image6" /> </LinearLayout> </ScrollView> </androidx.constraintlayout.widget.ConstraintLayout>
可调参数说明
- 缩放范围:修改
mScale.coerceIn(0.5f, 2f)中的两个数值即可调整最小、最大缩放比例 - 双击缩放:可以在
onDoubleTap回调中补充缩放逻辑,实现双击在1倍和最大缩放比之间快速切换 - 平移阻尼:如果需要平移时有阻尼效果,可以在计算
transX/transY时乘以一个小于1的系数即可
内容的提问来源于stack exchange,提问作者MediocreDevloper
相关产品推荐
相关产品推荐

