在 Android 中实现支持多手势交互的自定义 View(Kotlin 完整指南)

本文将手把手教你创建一个支持拖动、缩放、旋转等多种手势交互的自定义 View,并提供完整的代码实现和优化建议。

一、基础实现

1.1 创建自定义 View 骨架

import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.*class InteractiveView @JvmOverloads constructor(context: Context,attrs: AttributeSet? = null,defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {// 绘制相关private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {color = Color.BLUEstyle = Paint.Style.FILL}private var circleRadius = 100fprivate val originalRect = RectF()// 变换参数private var offsetX = 0fprivate var offsetY = 0fprivate var scaleFactor = 1fprivate var rotationAngle = 0f// 手势检测器private val gestureDetector: GestureDetectorprivate val scaleDetector: ScaleGestureDetectorprivate val rotationDetector: RotationGestureDetectorinit {// 初始化手势检测器gestureDetector = GestureDetector(context, GestureListener())scaleDetector = ScaleGestureDetector(context, ScaleListener())rotationDetector = RotationGestureDetector(context, RotationListener())}override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {super.onSizeChanged(w, h, oldw, oldh)originalRect.set(0f, 0f, w.toFloat(), h.toFloat())}override fun onDraw(canvas: Canvas) {super.onDraw(canvas)canvas.save()// 应用变换canvas.translate(offsetX, offsetY)canvas.scale(scaleFactor, scaleFactor, pivotX, pivotY)canvas.rotate(rotationAngle, pivotX, pivotY)// 绘制内容canvas.drawCircle(originalRect.centerX(),originalRect.centerY(),circleRadius,paint)canvas.restore()}override fun onTouchEvent(event: MotionEvent): Boolean {scaleDetector.onTouchEvent(event)rotationDetector.onTouchEvent(event)gestureDetector.onTouchEvent(event)return true}// 其他实现将在下文展开...
}

1.2 实现基本手势

拖动处理:
private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {override fun onScroll(e1: MotionEvent,e2: MotionEvent,distanceX: Float,distanceY: Float): Boolean {offsetX -= distanceXoffsetY -= distanceYapplyBoundaryConstraints()invalidate()return true}override fun onDoubleTap(e: MotionEvent): Boolean {// 双击重置变换resetTransformations()invalidate()return true}
}private fun resetTransformations() {offsetX = 0foffsetY = 0fscaleFactor = 1frotationAngle = 0f
}
缩放处理:
private var pivotX = 0f
private var pivotY = 0fprivate inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {pivotX = detector.focusXpivotY = detector.focusYreturn true}override fun onScale(detector: ScaleGestureDetector): Boolean {val scale = detector.scaleFactorscaleFactor *= scalescaleFactor = scaleFactor.coerceIn(0.1f, 5f)// 调整中心点偏移offsetX += (pivotX - offsetX) * (1 - scale)offsetY += (pivotY - offsetY) * (1 - scale)invalidate()return true}
}

二、高级手势实现

2.1 旋转手势检测器

class RotationGestureDetector(context: Context,private val listener: OnRotationGestureListener
) {private var prevAngle = 0ffun onTouchEvent(event: MotionEvent): Boolean {if (event.pointerCount != 2) return falsewhen (event.actionMasked) {MotionEvent.ACTION_POINTER_DOWN -> {prevAngle = getAngle(event)}MotionEvent.ACTION_MOVE -> {val newAngle = getAngle(event)listener.onRotate(newAngle - prevAngle)prevAngle = newAngle}}return true}private fun getAngle(event: MotionEvent): Float {val dx = event.getX(0) - event.getX(1)val dy = event.getY(0) - event.getY(1)return Math.toDegrees(atan2(dy.toDouble(), dx.toDouble())).toFloat()}interface OnRotationGestureListener {fun onRotate(angleDelta: Float)}
}// 在 InteractiveView 中添加:
private inner class RotationListener : RotationGestureDetector.OnRotationGestureListener {override fun onRotate(angleDelta: Float) {rotationAngle += angleDeltarotationAngle %= 360invalidate()}
}

2.2 边界约束

private fun applyBoundaryConstraints() {val scaledWidth = originalRect.width() * scaleFactorval scaledHeight = originalRect.height() * scaleFactorval maxOffsetX = (scaledWidth - originalRect.width()) / 2val maxOffsetY = (scaledHeight - originalRect.height()) / 2offsetX = offsetX.coerceIn(-maxOffsetX, maxOffsetX)offsetY = offsetY.coerceIn(-maxOffsetY, maxOffsetY)
}

三、性能优化实现

惯性滑动

private val scroller = Scroller(context)override fun computeScroll() {if (scroller.computeScrollOffset()) {offsetX = scroller.currX.toFloat()offsetY = scroller.currY.toFloat()applyBoundaryConstraints()invalidate()}
}private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {override fun onFling(e1: MotionEvent,e2: MotionEvent,velocityX: Float,velocityY: Float): Boolean {scroller.fling(offsetX.toInt(),offsetY.toInt(),velocityX.toInt(),velocityY.toInt(),Int.MIN_VALUE,Int.MAX_VALUE,Int.MIN_VALUE,Int.MAX_VALUE)invalidate()return true}
}

四、完整布局示例

<com.example.app.InteractiveViewandroid:id="@+id/interactiveView"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#F0F0F0"/>

五、最佳实践建议

  1. 绘制优化:

    override fun onDraw(canvas: Canvas) {// 避免在绘制过程中创建新对象canvas.drawCircle(originalRect.centerX(),originalRect.centerY(),circleRadius,paint // 重用预定义的 Paint 对象)
    }
    
  2. 手势优先级处理:

    override fun onTouchEvent(event: MotionEvent): Boolean {when {scaleDetector.isInProgress -> scaleDetector.onTouchEvent(event)rotationDetector.isInProgress -> rotationDetector.onTouchEvent(event)else -> gestureDetector.onTouchEvent(event)}return true
    }
    
  3. 多指触控处理:

    private var activePointerId = MotionEvent.INVALID_POINTER_IDoverride fun onTouchEvent(event: MotionEvent): Boolean {when (event.actionMasked) {MotionEvent.ACTION_POINTER_DOWN -> {activePointerId = event.getPointerId(event.actionIndex)}MotionEvent.ACTION_POINTER_UP -> {val remainingPointer = if (event.actionIndex == 0) 1 else 0activePointerId = event.getPointerId(remainingPointer)}}// 其他手势处理...
    }
    

六、完整功能集成

最终的自定义 View 将支持:

  • 单指拖动
  • 双指缩放(带锚点)
  • 双指旋转
  • 惯性滑动
  • 双击重置
  • 边界约束
  • 多指触控支持

可以通过组合这些手势实现复杂的交互效果,如图片查看器、可操作图表等。


以下是为您整合所有功能的完整实现代码:

import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.*
import android.widget.OverScroller
import kotlin.math.*class AdvancedInteractiveView @JvmOverloads constructor(context: Context,attrs: AttributeSet? = null,defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {// 绘制参数private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {color = Color.BLUEstyle = Paint.Style.FILL}private var circleRadius = 100fprivate val originalRect = RectF()// 变换参数private var offsetX = 0fprivate var offsetY = 0fprivate var scaleFactor = 1fprivate var rotationAngle = 0fprivate var pivotX = 0fprivate var pivotY = 0f// 手势检测器private val gestureDetector: GestureDetectorprivate val scaleDetector: ScaleGestureDetectorprivate val rotationDetector: RotationGestureDetectorprivate val scroller = OverScroller(context)// 边界约束参数private var minScale = 0.5fprivate var maxScale = 5fprivate var isScaling = falseprivate var activePointerId = MotionEvent.INVALID_POINTER_IDinit {gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {override fun onScroll(e1: MotionEvent,e2: MotionEvent,distanceX: Float,distanceY: Float): Boolean {if (!isScaling) {offsetX -= distanceXoffsetY -= distanceYapplyBoundaryConstraints()invalidate()}return true}override fun onDoubleTap(e: MotionEvent): Boolean {resetTransformations()invalidate()return true}override fun onFling(e1: MotionEvent,e2: MotionEvent,velocityX: Float,velocityY: Float): Boolean {scroller.fling(offsetX.toInt(),offsetY.toInt(),velocityX.toInt(),velocityY.toInt(),Int.MIN_VALUE,Int.MAX_VALUE,Int.MIN_VALUE,Int.MAX_VALUE,100,100)invalidate()return true}})scaleDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() {override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {isScaling = truepivotX = detector.focusXpivotY = detector.focusYreturn true}override fun onScale(detector: ScaleGestureDetector): Boolean {val scale = detector.scaleFactorval newScale = scaleFactor * scaleif (newScale in minScale..maxScale) {// 调整偏移量保持锚点位置offsetX += (pivotX - offsetX) * (1 - scale)offsetY += (pivotY - offsetY) * (1 - scale)scaleFactor = newScale}applyBoundaryConstraints()invalidate()return true}override fun onScaleEnd(detector: ScaleGestureDetector) {isScaling = false}})rotationDetector = RotationGestureDetector(object : RotationGestureDetector.OnRotationGestureListener {override fun onRotate(angleDelta: Float) {rotationAngle += angleDeltarotationAngle %= 360invalidate()}})}override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {super.onSizeChanged(w, h, oldw, oldh)originalRect.set(0f, 0f, w.toFloat(), h.toFloat())resetTransformations()}override fun onDraw(canvas: Canvas) {super.onDraw(canvas)canvas.save()// 应用变换矩阵canvas.translate(offsetX, offsetY)canvas.scale(scaleFactor, scaleFactor, pivotX, pivotY)canvas.rotate(rotationAngle, pivotX, pivotY)// 绘制圆形canvas.drawCircle(originalRect.centerX(),originalRect.centerY(),circleRadius,paint)canvas.restore()}override fun computeScroll() {if (scroller.computeScrollOffset()) {offsetX = scroller.currX.toFloat()offsetY = scroller.currY.toFloat()applyBoundaryConstraints()invalidate()}}override fun onTouchEvent(event: MotionEvent): Boolean {scaleDetector.onTouchEvent(event)gestureDetector.onTouchEvent(event)rotationDetector.onTouchEvent(event)handleMultiTouch(event)return true}private fun handleMultiTouch(event: MotionEvent) {when (event.actionMasked) {MotionEvent.ACTION_POINTER_DOWN -> {activePointerId = event.getPointerId(event.actionIndex)}MotionEvent.ACTION_POINTER_UP -> {val remainingPointer = if (event.actionIndex == 0) 1 else 0activePointerId = event.getPointerId(remainingPointer)}}}private fun applyBoundaryConstraints() {val viewWidth = originalRect.width()val viewHeight = originalRect.height()val scaledWidth = viewWidth * scaleFactorval scaledHeight = viewHeight * scaleFactorval maxOffsetX = (scaledWidth - viewWidth) / 2val maxOffsetY = (scaledHeight - viewHeight) / 2offsetX = offsetX.coerceIn(-maxOffsetX, maxOffsetX)offsetY = offsetY.coerceIn(-maxOffsetY, maxOffsetY)}private fun resetTransformations() {offsetX = 0foffsetY = 0fscaleFactor = 1frotationAngle = 0fpivotX = originalRect.centerX()pivotY = originalRect.centerY()invalidate()}// 自定义旋转手势检测器private class RotationGestureDetector(private val listener: OnRotationGestureListener) {private var prevAngle = 0ffun onTouchEvent(event: MotionEvent): Boolean {if (event.pointerCount != 2) return falsewhen (event.actionMasked) {MotionEvent.ACTION_POINTER_DOWN -> prevAngle = getAngle(event)MotionEvent.ACTION_MOVE -> {val newAngle = getAngle(event)listener.onRotate(newAngle - prevAngle)prevAngle = newAngle}}return true}private fun getAngle(event: MotionEvent): Float {val dx = event.getX(0) - event.getX(1)val dy = event.getY(0) - event.getY(1)return Math.toDegrees(atan2(dy.toDouble(), dx.toDouble())).toFloat()}interface OnRotationGestureListener {fun onRotate(angleDelta: Float)}}
}

功能实现说明

  1. 单指拖动

    • 通过GestureDetector检测滚动事件
    • onScroll中更新offsetX/Y
    • 添加边界约束防止移出可视区域
  2. 双指缩放(带锚点)

    • 使用ScaleGestureDetector检测缩放手势
    • 记录缩放锚点(pivotX, pivotY)
    • 动态调整偏移量保持锚点位置稳定
    • 限制缩放范围(0.5-5倍)
  3. 双指旋转

    • 自定义RotationGestureDetector计算旋转角度
    • 通过两点坐标计算旋转角度差值
    • 更新rotationAngle并限制在0-360度之间
  4. 惯性滑动

    • 使用OverScroller实现流畅的惯性滑动
    • onFling中初始化滑动参数
    • computeScroll中持续更新位置
  5. 双击重置

    • onDoubleTap中重置所有变换参数
    • 重置位置、缩放、旋转到初始状态
  6. 边界约束

    • applyBoundaryConstraints方法计算最大偏移量
    • 根据当前缩放比例动态调整边界限制
    • 在每次位置变化后调用约束方法
  7. 多指触控支持

    • 处理ACTION_POINTER_DOWN/UP事件
    • 跟踪活动指针ID
    • 正确处理多指手势的切换

使用方式

  1. 在XML布局中添加:
<com.your.package.AdvancedInteractiveViewandroid:layout_width="match_parent"android:layout_height="match_parent"android:background="#F0F0F0"/>
  1. 自定义属性建议(可选):
<resources><declare-styleable name="AdvancedInteractiveView"><attr name="minScale" format="float" /><attr name="maxScale" format="float" /><attr name="shapeColor" format="color" /></declare-styleable>
</resources>

性能优化建议

  1. 硬件加速
<application android:hardwareAccelerated="true">
  1. 绘制优化
  • 避免在onDraw中创建新对象
  • 使用canvas.saveLayer()替代多次绘制
  • 对于复杂图形使用Bitmap缓存
  1. 手势优化
  • 设置合适的手势检测阈值
  • 使用ViewConfiguration获取系统标准值:
private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
  1. 内存管理
  • 在onDetachedFromWindow中释放资源
  • 使用弱引用持有上下文

可根据需要调整以下参数:

  • circleRadius:初始圆形半径
  • minScale/maxScale:缩放范围限制
  • 颜色和样式通过Paint对象自定义
  • 边界约束计算逻辑调整

实际使用时可扩展以下功能:

  • 添加更多图形元素
  • 实现手势冲突解决策略
  • 添加触摸反馈动画
  • 支持更多手势(如长按、快速滑动等)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/81762.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Kotlin 协程 (一)

1. Kotlin 协程的核心概念 1.1 协程&#xff08;Coroutine&#xff09; 定义&#xff1a;协程是一种轻量级的执行上下文&#xff0c;可以在任何时候挂起和恢复&#xff0c;而不需要阻塞线程。特点&#xff1a; 比传统线程更轻量&#xff0c;开销更小。支持挂起和恢复&#xf…

机器学习 集成学习方法之随机森林

集成学习方法之随机森林 1 集成学习2 随机森林的算法原理2.1 Sklearn API2.2 示例 1 集成学习 机器学习中有一种大类叫集成学习&#xff08;Ensemble Learning&#xff09;&#xff0c;集成学习的基本思想就是将多个分类器组合&#xff0c;从而实现一个预测效果更好的集成分类…

thinkphp6实现统一监听并记录所有执行的sql语句除查询外

创建文件app/middleware/SqlLogger.php <?php namespace app\middleware;use think\facade\Db; use think\facade\Session;class SqlLogger {public function handle($request, \Closure $next){// 监听所有SQL $request->ip()Db::listen(function($sql, $time) {if (p…

pytorch训练可视化工具---TensorBoard

一、目的&#xff1a;为什么使用 TensorBoard 调控模型 使用 TensorBoard 可以帮我们&#xff1a; 实时查看 loss / acc 曲线 → 判断是否过拟合、欠拟合&#xff1b; 对比不同模型或超参数的效果&#xff1b; 可视化模型结构 → 帮助调试模型设计&#xff1b; 查看权重/梯…

机器学习知识自然语言处理入门

一、引言&#xff1a;当文字遇上数学 —— 自然语言的数字化革命 在自然语言处理&#xff08;NLP&#xff09;的世界里&#xff0c;计算机要理解人类语言&#xff0c;首先需要将文字转化为数学向量。早期的 One-Hot 编码如同给每个词语分配一个唯一的 “房间号”&#xff0c;例…

Linux-线程概念和控制

1.Linux线程概念 1.1什么是线程 • 在⼀个程序⾥的⼀个执⾏路线就叫做线程&#xff08;thread&#xff09;。更准确的定义是&#xff1a;线程是“⼀个进程内部 的控制序列” • ⼀切进程⾄少都有⼀个执⾏线程 • 线程在进程内部运⾏&#xff0c;本质是在进程地址空间内运⾏…

【氮化镓】低剂量率对GaN HEMT栅极漏电的影响

2024 年 2 月 22 日,中国科学院新疆理化技术研究所的Li等人在《IEEE ACCESS》期刊发表了题为《Degradation Mechanisms of Gate Leakage in GaN-Based HEMTs at Low Dose Rate Irradiation》的文章,基于实验分析和 TCAD 仿真,研究了低剂量率辐照下基于 GaN 的 p 型栅高电子迁…

.NET Core 中 Swagger 配置详解:常用配置与实战技巧

随着微服务架构和 RESTful API 的广泛应用&#xff0c;API 文档的管理和自动化生成成为了开发中的重要部分。Swagger&#xff08;现为 OpenAPI&#xff09;是一款功能强大的工具&#xff0c;它可以自动生成 API 文档&#xff0c;并提供交互式 UI&#xff0c;帮助开发者、测试人…

海康工业相机白平衡比选择器对应的值被重置后,如何恢复原成像

做项目的时候&#xff0c;有时候手抖&#xff0c;一不小心把一个成熟稳定的项目的相机配置&#xff0c;重置了&#xff0c;如何进行恢复呢&#xff0c;在不知道之前配置数据的情况下。 我在做项目的时候&#xff0c;为了让这个相机成像稳定一点&#xff0c;尤其是做颜色检测时…

【八股战神篇】Java虚拟机(JVM)高频面试题

目录 专栏简介 一 请解释Java虚拟机(JVM)及其主要功能 延伸 1. JVM的基本概念 2. JVM的主要功能 二 对象创建的过程了解吗 延伸 1.Java 创建对象的四种常见方式 三 什么是双亲委派模型 延伸 1.双亲委派机制的作用: 2.双亲委派模型的核心思想: 3.双亲委派模型的…

win10 上删除文件夹失败的一个原因:sqlYog 备份/导出关联了该文件夹

在尝试删除路径为.../bak/sql的文件时&#xff0c;系统提示无权限操作。然而&#xff0c;关闭SQLyog后&#xff0c;删除操作成功完成。这表明SQLyog可能正在占用该文件&#xff0c;导致删除权限受限。关闭SQLyog后&#xff0c;文件被释放&#xff0c;删除操作得以顺利进行。建议…

Oracle中如何解决LATCH:CACHE BUFFERS LRU CHAIN

简单来讲&#xff0c;Oracle为了高效管理BUFFER CACHE主要使用以下2种LRU列&#xff1a; LRU列&#xff0c;又叫替换列&#xff08;replacement list&#xff09;&#xff0c;其中又分为主列和辅助列。 主列&#xff1a;已使用的缓冲区列&#xff0c;分为HOT和COLD区域。HOT区…

C++:迭代器

迭代器的本质&#xff1a;对象。 迭代器与指针类似&#xff0c;通过迭代器可以指向容器中的某个元素&#xff0c;还可以对元素进行操作。 迭代器统一规范了遍历方式。不同的数据结构可以用统一的方式去遍历。 接下来是一个自定义迭代器的代码示例。 #include<iostream&g…

(4)Java虚拟线程与传统线程对比

虚拟线程与传统线程对比 &#x1f504; &#x1f4cb; 核心问题 Project Loom的虚拟线程与传统线程在资源消耗上有何区别&#xff1f;如何设计一个支持百万级并发的服务&#xff1f; &#x1f4ca; 资源消耗比较 &#x1f418; 传统线程 &#x1f4cf; 每线程约1MB栈空间&am…

Java 单元测试框架比较:JUnit、TestNG 哪个更适合你?

Java 单元测试框架比较&#xff1a;JUnit、TestNG 哪个更适合你&#xff1f; 在 Java 开发领域&#xff0c;单元测试是保证代码质量的重要环节。而选择一个合适的单元测试框架&#xff0c;对于提升测试效率和代码可靠性至关重要。本文将深入比较 JUnit 和 TestNG 这两个主流的…

从零开始的抽奖系统创作(2)

我们接着进行抽奖系统的完善。 前面我们完成了 1.结构初始化&#xff08;统一结果返回之类的&#xff0c;还有包的分类&#xff09; 2.加密&#xff08;基于Hutool进行的对称与非对称加密&#xff09; 3.用户注册 接下来我们先完善一下结构&#xff08;统一异常处理&#…

【vs2022的C#窗体项目】打开运行+sql Server改为mysql数据库+发布

1. vs2022打开运行原sql Server的C#窗体项目更改为mysql数据库 1.1. vs2022安装基础模块即可 安装1️⃣vs核心编辑器2️⃣.net桌面开发必选&#xff0c;可选均不安装&#xff01;&#xff01;&#xff01; 为了成功连接mysql数据库&#xff0c;需要安装组件NuGet包管理器 安…

AI 编程 “幻觉” 风险频发?飞算 JavaAI 硬核技术筑牢安全防线

AI 技术已深度融入编程领域&#xff0c;为开发者带来前所未有的便利与效率提升。然而&#xff0c;AI 编程 “幻觉” 问题如影随形&#xff0c;频频引发困扰&#xff0c;成为阻碍行业稳健发展的潜在风险。飞算 JavaAI 凭借一系列硬核技术&#xff0c;强势出击&#xff0c;为攻克…

数据库----软考中级软件设计师(自用学习笔记)

目录 1、E-R图 2、结构数据模型 3、数据库的三级模式结构 4、关系代数 5、查询 6、SQL控制语句 7、视图​编辑 8、索引 9、关系模式 10、函数依赖 11、通过闭包求候选码 12、范式 13、无损连接和保持函数依赖 14、数据库设计 15、数据库的控制功能 16、数据库…

【Qt】Qt常见控件的相关知识点

1.close退出槽函数 2.设置快捷键&#xff0c;QMenu 。 适用&字母就能设置快捷键&#xff0c;运行qt程序&#xff0c;最后就可以按Alt对应的字母进行快捷操作。 3.QMenuBar内存泄露问题 如果ui已经自动生成了menubar&#xff0c;我们再次生成一个新的菜单栏&#xff0c;而…