You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 13:15:42