Android 双向滑动

复制代码
GestureDetector //系统
GestureDetectorCompat //AndroidX 较旧机型有更好的支持

code

复制代码
package com.example.myapplication.view

import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.view.View
import android.widget.OverScroller
import androidx.core.view.GestureDetectorCompat
import androidx.core.view.ViewCompat
import com.example.myapplication.dp
import com.example.myapplication.getAvatar
import kotlin.math.max
import kotlin.math.min

private val IMAGE_SIZE = 300.dp.toInt()
private const val EXTRA_SCALE_FACTOR = 1.5f

class ScalableImageView(context: Context, attrs: AttributeSet) : View(context, attrs) {

    private val runnable = FlingRunner()
    private val bitmap = getAvatar(resources, IMAGE_SIZE)
    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    private var simpleGenListener = SimpleGenListener()
    private val scaleListener = SimpleScaleListener()
    private val scaleGenstureDetector =
        ScaleGestureDetector(context, scaleListener) //ScaleGestureDetectorCompat是扩展

    private var originalOffsetX = 0f
    private var originalOffsetY = 0f



    private var offsetX = 0f //偏移
    private var offsetY = 0f

    private var smallScale = 0f //小图
    private var bigScale = 0f //大图

    private val scaleAnimation = ObjectAnimator.ofFloat(this, "currentScale", smallScale, bigScale)
    private val gestureDetector = GestureDetectorCompat(context, simpleGenListener)  //gestureDetector.setIsLongpressEnabled(false) //关闭长按
    private var scroller = OverScroller(context) //滑动fling
    private var big = false //双击开关

    private var currentScale = 0f
        set(value) {
            field = value
            invalidate()
        }


    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        originalOffsetX = (width - bitmap.width) / 2f
        originalOffsetY = (height - bitmap.height) / 2f

        if (bitmap.width / bitmap.height.toFloat() > width / height.toFloat()) {
            smallScale = width / bitmap.width.toFloat()
            bigScale = height / bitmap.height.toFloat() * EXTRA_SCALE_FACTOR
        } else {
            smallScale = height / bitmap.height.toFloat()
            bigScale = width / bitmap.width.toFloat() * EXTRA_SCALE_FACTOR
        }

        currentScale = smallScale
        scaleAnimation.setFloatValues(smallScale, bigScale)
    }


    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        val scaleFraction = (currentScale - smallScale) / (bigScale - smallScale)
        canvas.translate(offsetX * scaleFraction, offsetY * scaleFraction)
//        val scale = smallScale + (bigScale - smallScale) * scaleFraction
        //scale 改变后 坐标系也会改变
//        canvas.scale(scale, scale, width / 2f, height / 2f)
        canvas.scale(currentScale, currentScale, width / 2f, height / 2f)
        canvas.drawBitmap(bitmap, originalOffsetX, originalOffsetY, paint)
    }

    private fun fixOffsets() {
        offsetX = min(offsetX, (bitmap.width * bigScale - width) / 2)
        offsetX = max(offsetX, -(bitmap.width * bigScale - width) / 2)
        offsetY = min(offsetY, (bitmap.height * bigScale - height) / 2)
        offsetY = max(offsetY, -(bitmap.height * bigScale - height) / 2)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        scaleGenstureDetector.onTouchEvent(event)
        if (!scaleGenstureDetector.isInProgress){
            gestureDetector.onTouchEvent(event)
        }
        return true
    }

    inner class SimpleScaleListener : ScaleGestureDetector.OnScaleGestureListener {
        override fun onScale(detector: ScaleGestureDetector): Boolean {
            val tempCurrentScale =  currentScale * detector.scaleFactor
            if (tempCurrentScale < smallScale || tempCurrentScale > bigScale){
                return false
            }else{
                currentScale *= detector.scaleFactor //0 max
                return true
            }

//            currentScale = currentScale.coerceAtLeast(smallScale).coerceAtMost(bigScale) //最大值
//            return true //是否统计刚才的值是否拿到
        }

        override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
            offsetX = (detector.focusX - width / 2f) * (1 - bigScale / smallScale)
            offsetY = (detector.focusY - height / 2f) * (1 - bigScale / smallScale)
            fixOffsets()
            return true
        }

        override fun onScaleEnd(detector: ScaleGestureDetector?) {

        }

    }

    //  GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener,
    inner class SimpleGenListener : GestureDetector.SimpleOnGestureListener() {
        //快速滑动 velocityX 速率 位移 矢量长度
        override fun onFling(
            downEvent: MotionEvent?,
            currentEvent: MotionEvent?,
            velocityX: Float,
            velocityY: Float
        ): Boolean {
            if (big) {
                scroller.fling(
                    offsetX.toInt(),
                    offsetY.toInt(),
                    velocityX.toInt(),
                    velocityY.toInt(),
                    (-(bitmap.width * bigScale - width) / 2).toInt(),
                    ((bitmap.width * bigScale - width) / 2).toInt(),
                    (-(bitmap.height * bigScale - height) / 2).toInt(),
                    ((bitmap.height * bigScale - height) / 2).toInt()
                )

                //下一帧调用 刷新
                ViewCompat.postOnAnimation(this@ScalableImageView, runnable)
            }

            return false
        }

        //onMove actionMove distanceX旧位置-新位置
        override fun onScroll(
            downEvent: MotionEvent?,
            cuuentEvent: MotionEvent?,
            distanceX: Float,
            distanceY: Float
        ): Boolean {
            if (big) {
                offsetX -= distanceX
                offsetY -= distanceY
                fixOffsets()
                invalidate()
            }

            return false
        }

        //双击 判断依据 300ms,40ms以下不处理 防手抖
        override fun onDoubleTap(e: MotionEvent): Boolean {
            big = !big
            if (big) {
                offsetX = (e.x - width / 2) * (1 - bigScale / smallScale)
                offsetY = (e.y - height / 2) * (1 - bigScale / smallScale)
                fixOffsets()
                scaleAnimation.start()
            } else {

                scaleAnimation.reverse()
            }

            return true
        }

//        //双击之后的后续操作
//        override fun onDoubleTapEvent(e: MotionEvent?): Boolean {
//            return false
//        }

        //长按
//        override fun onLongPress(e: MotionEvent?) {}
//        override fun onShowPress(e: MotionEvent?) {}

//        //不是长按 也不是双击回调 等300ms 没按下 会回调 双击时回调使用准备 时间不够及时
//        override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
//            return false
//        }

        override fun onDown(e: MotionEvent?): Boolean {
            return true
        }

//        //单次点击抬起 判断是否是快速抬起给系统做决定 true false 不影响结果 单击
//        override fun onSingleTapUp(e: MotionEvent?): Boolean {
//            return false
//        }
    }

    inner class FlingRunner : Runnable {
        override fun run() {
            //当前位置 速度
            if (scroller.computeScrollOffset()) {
                offsetX = scroller.currX.toFloat()
                offsetY = scroller.currY.toFloat()
                invalidate()
                ViewCompat.postOnAnimation(this@ScalableImageView, this)
            }

        }

    }
}
相关推荐
aqi009 小时前
鸿蒙版本的电子书阅读APP开放源码啦
android·华为·ai编程·harmonyos·鸿蒙
墨天梦9 小时前
B19_SunnyWeather到阅读客户端
android·kotlin
美狐美颜sdk10 小时前
直播APP源码可以直接接入视频美颜sdk吗?技术方案详解
android·人工智能·音视频·美颜sdk·直播美颜sdk
用户693717500138413 小时前
2026,程序员的时代拐点到了
android·前端·后端
浪潮IT馆15 小时前
Android Studio 非最新版本下载安装教程
android·ide·android studio
xiaozongt198916 小时前
MasterGo + Claude Code 生成 Android 布局:完整实践指南
android
Anhty17 小时前
2026最新免费手机音频处理工具 !!
android·功能测试·ios·智能手机·音视频
2501_9159090619 小时前
iOS应用从开发到上架App Store的完整发布流程与步骤指南
android·ios·小程序·https·uni-app·webview
狗凯之家源码网20 小时前
AI 步数修改提交系统源码应用场景与落地指南
android·数据库·人工智能·运动步数
ZHOUPUYU20 小时前
PHP 9.0 前瞻:下一代 PHP 会带来哪些新功能?
android·开发语言·php